merge: allow SpawnPod child cwd
# Conflicts: # crates/pod/src/pod.rs
This commit is contained in:
commit
05df656616
|
|
@ -119,7 +119,7 @@ reviewer には coder の実装方針ではなく、この intent packet と dif
|
||||||
- child worktree path / branch
|
- child worktree path / branch
|
||||||
- 対象 ticket path
|
- 対象 ticket path
|
||||||
- intent packet
|
- intent packet
|
||||||
- Bash は必ず child worktree に `cd` すること
|
- SpawnPod の `cwd` は child worktree に設定すること(`cwd` は process/tool default cwd であり、scope/authority ではない)
|
||||||
- main workspace の `TODO.md` / `tickets/` / `docs/report/` / `.yoi` は編集しないこと
|
- main workspace の `TODO.md` / `tickets/` / `docs/report/` / `.yoi` は編集しないこと
|
||||||
- child worktree 内の tracked `.yoi` project records は実装対象に必要な branch-local artifacts/dossiers として編集してよいが、`.yoi/memory` や local/runtime/secret-like files は作らないこと
|
- child worktree 内の tracked `.yoi` project records は実装対象に必要な branch-local artifacts/dossiers として編集してよいが、`.yoi/memory` や local/runtime/secret-like files は作らないこと
|
||||||
- active orchestration progress と最終 review/approval/close は main workspace の責任として残すこと
|
- active orchestration progress と最終 review/approval/close は main workspace の責任として残すこと
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ fi
|
||||||
|
|
||||||
## Pod へ渡す scope
|
## Pod へ渡す scope
|
||||||
|
|
||||||
Pod を使う場合、Pod の cwd は main workspace のままになる。必ず作業対象が child worktree であることを明示し、Bash 実行時は毎回 `cd <repo>/.worktree/<task-name> && ...` させる。
|
Pod を使う場合、coder Pod の SpawnPod `cwd` は child worktree に設定する。`cwd` は child process/tool default cwd だけを変え、runtime workspace root・project record root・scope/authority は変えないため、下の明示 scope は別途渡す。
|
||||||
|
|
||||||
coder Pod 推奨 scope:
|
coder Pod 推奨 scope:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -550,7 +550,7 @@ fn append_orchestrator_agent_routing_guidance(out: &mut String) {
|
||||||
out.push_str("- Create worktrees or spawn coder/reviewer Pods only after `workflow_state = inprogress` is already recorded and accepted. If the Ticket is still queued and unblocked, record `queued -> inprogress` before any worktree/SpawnPod side effect.\n");
|
out.push_str("- Create worktrees or spawn coder/reviewer Pods only after `workflow_state = inprogress` is already recorded and accepted. If the Ticket is still queued and unblocked, record `queued -> inprogress` before any worktree/SpawnPod side effect.\n");
|
||||||
out.push_str("- Use `worktree-workflow` for the mechanical worktree plan: create `.worktree/<task-name>`, keep tracked `.yoi` project records visible in the child worktree, exclude `.yoi/memory` plus local/runtime/log/lock/secret-like `.yoi` paths, and keep active orchestration progress plus final review/approval/close in the main workspace unless explicitly designed otherwise.\n");
|
out.push_str("- Use `worktree-workflow` for the mechanical worktree plan: create `.worktree/<task-name>`, keep tracked `.yoi` project records visible in the child worktree, exclude `.yoi/memory` plus local/runtime/log/lock/secret-like `.yoi` paths, and keep active orchestration progress plus final review/approval/close in the main workspace unless explicitly designed otherwise.\n");
|
||||||
out.push_str("- Use `multi-agent-workflow` for the sibling loop: coder and reviewer are siblings under this Orchestrator; coder gets narrow write scope to the child worktree; reviewer is read-only by default.\n");
|
out.push_str("- Use `multi-agent-workflow` for the sibling loop: coder and reviewer are siblings under this Orchestrator; coder gets narrow write scope to the child worktree; reviewer is read-only by default.\n");
|
||||||
out.push_str("- Give the coder an intent packet that distinguishes binding decisions/invariants, implementation latitude, escalation conditions, child worktree/branch, validation commands, and report expectations; require Bash commands to `cd` into the child worktree, prohibit editing main-workspace `.yoi`/Ticket/workflow/docs records, and prohibit creating generated memory/local/runtime/secret-like files in the child worktree.\n");
|
out.push_str("- Give the coder an intent packet that distinguishes binding decisions/invariants, implementation latitude, escalation conditions, child worktree/branch, validation commands, and report expectations; set SpawnPod `cwd` to the child worktree while delegating explicit scope separately, prohibit editing main-workspace `.yoi`/Ticket/workflow/docs records, and prohibit creating generated memory/local/runtime/secret-like files in the child worktree.\n");
|
||||||
out.push_str("- Give the reviewer the recorded Ticket intent, binding decisions/invariants, implementation latitude, acceptance criteria, explicit escalation conditions, diff/commits, validation evidence, and blocker/non-blocker criteria; reviewer judgment is against recorded requirements and decisions, not unrecorded preferred tactics. Keep branch-local reviewer verdicts in the review report or merge-ready dossier rather than recording them as final main-branch Ticket approval.\n");
|
out.push_str("- Give the reviewer the recorded Ticket intent, binding decisions/invariants, implementation latitude, acceptance criteria, explicit escalation conditions, diff/commits, validation evidence, and blocker/non-blocker criteria; reviewer judgment is against recorded requirements and decisions, not unrecorded preferred tactics. Keep branch-local reviewer verdicts in the review report or merge-ready dossier rather than recording them as final main-branch Ticket approval.\n");
|
||||||
out.push_str("- Ticket thread progress may record worktree plan, coder delegated/completed/blocked, reviewer delegated, blocker/fix-loop summaries, and merge-ready dossier pointer; do not merge, close, or record final main approval in this routing/branch-review phase.\n");
|
out.push_str("- Ticket thread progress may record worktree plan, coder delegated/completed/blocked, reviewer delegated, blocker/fix-loop summaries, and merge-ready dossier pointer; do not merge, close, or record final main approval in this routing/branch-review phase.\n");
|
||||||
out.push_str("- Stop at a merge-ready dossier for `orchestrator-merge-completion` containing Ticket id/slug, branch/worktree, commits, intent/invariant check, implementation summary, coder/reviewer Pods, blockers fixed or rejected findings with reasons, validation performed, residual risks, dirty state, and parent/human decision needs if any.\n");
|
out.push_str("- Stop at a merge-ready dossier for `orchestrator-merge-completion` containing Ticket id/slug, branch/worktree, commits, intent/invariant check, implementation summary, coder/reviewer Pods, blockers fixed or rejected findings with reasons, validation performed, residual risks, dirty state, and parent/human decision needs if any.\n");
|
||||||
|
|
@ -567,7 +567,7 @@ fn append_orchestrator_agent_routing_guidance(out: &mut String) {
|
||||||
|
|
||||||
fn append_coder_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("\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("- 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("- 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");
|
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");
|
||||||
|
|
|
||||||
|
|
@ -508,6 +508,7 @@ where
|
||||||
// below so the worker borrow doesn't conflict with reads on `pod`.
|
// below so the worker borrow doesn't conflict with reads on `pod`.
|
||||||
let scope_handle = pod.scope().clone();
|
let scope_handle = pod.scope().clone();
|
||||||
let pwd = pod.pwd().to_path_buf();
|
let pwd = pod.pwd().to_path_buf();
|
||||||
|
let workspace_root = pod.workspace_root().to_path_buf();
|
||||||
let task_feature = pod.task_feature();
|
let task_feature = pod.task_feature();
|
||||||
let session_id_for_usage = pod.segment_id().to_string();
|
let session_id_for_usage = pod.segment_id().to_string();
|
||||||
let memory_config = pod.manifest().memory.clone();
|
let memory_config = pod.manifest().memory.clone();
|
||||||
|
|
@ -538,7 +539,9 @@ where
|
||||||
|
|
||||||
let mut feature_registry = FeatureRegistryBuilder::new();
|
let mut feature_registry = FeatureRegistryBuilder::new();
|
||||||
feature_registry.add_module(task_feature);
|
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 _feature_install_report = pod.install_features(feature_registry);
|
||||||
|
|
||||||
let worker = pod.worker_mut();
|
let worker = pod.worker_mut();
|
||||||
|
|
@ -549,7 +552,7 @@ where
|
||||||
// their built-in linter. Companion deny rules on the generic CRUD
|
// their built-in linter. Companion deny rules on the generic CRUD
|
||||||
// scope were already applied during `Pod::from_manifest`.
|
// scope were already applied during `Pod::from_manifest`.
|
||||||
if let Some(mem) = memory_config.as_ref() {
|
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);
|
let query_cfg = memory::tool::QueryConfig::from(mem);
|
||||||
worker.register_tool(memory::tool::read_tool_with_usage(
|
worker.register_tool(memory::tool::read_tool_with_usage(
|
||||||
layout.clone(),
|
layout.clone(),
|
||||||
|
|
@ -569,6 +572,7 @@ where
|
||||||
spawner_name.clone(),
|
spawner_name.clone(),
|
||||||
spawner_socket,
|
spawner_socket,
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
|
workspace_root.clone(),
|
||||||
pwd.clone(),
|
pwd.clone(),
|
||||||
spawned_registry.clone(),
|
spawned_registry.clone(),
|
||||||
self_parent_socket,
|
self_parent_socket,
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ struct Cli {
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
workspace: Option<PathBuf>,
|
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.
|
/// Manifest TOML to use directly as a one-file compatibility/debug input.
|
||||||
/// This bypasses profile discovery but still applies builtin defaults and
|
/// This bypasses profile discovery but still applies builtin defaults and
|
||||||
/// the same required-field validation boundary.
|
/// the same required-field validation boundary.
|
||||||
|
|
@ -101,6 +107,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 {
|
fn runtime_pod_name(cli: &Cli, workspace_root: &Path) -> String {
|
||||||
cli.pod
|
cli.pod
|
||||||
.as_deref()
|
.as_deref()
|
||||||
|
|
@ -355,8 +372,33 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match Pod::from_manifest_spawned(manifest, store, loader, callback).await {
|
let tool_cwd = match runtime_tool_cwd(&cli, &workspace_root) {
|
||||||
Ok(p) => p,
|
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) => {
|
Err(e) => {
|
||||||
eprintln!("error: failed to create spawned pod: {e}");
|
eprintln!("error: failed to create spawned pod: {e}");
|
||||||
return ExitCode::FAILURE;
|
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
|
/// `segment_id` and append tally. `self.segment_id()` is a thin
|
||||||
/// wrapper over `segment_state.segment_id()`.
|
/// wrapper over `segment_state.segment_id()`.
|
||||||
segment_state: Arc<SegmentState>,
|
segment_state: Arc<SegmentState>,
|
||||||
/// Absolute working directory of the Pod.
|
/// Absolute tool/process working directory of the Pod.
|
||||||
pwd: PathBuf,
|
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.
|
/// Shared, atomically-swappable view of the Pod's resolved scope.
|
||||||
/// Cloned out to `ScopedFs` instances (builtin tools, fs_view,
|
/// Cloned out to `ScopedFs` instances (builtin tools, fs_view,
|
||||||
/// compact worker) so scope updates propagate to every consumer
|
/// compact worker) so scope updates propagate to every consumer
|
||||||
|
|
@ -421,6 +424,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
|
||||||
pod_metadata_writer: None,
|
pod_metadata_writer: None,
|
||||||
segment_state: self.segment_state.clone(),
|
segment_state: self.segment_state.clone(),
|
||||||
pwd: self.pwd.clone(),
|
pwd: self.pwd.clone(),
|
||||||
|
workspace_root: self.workspace_root.clone(),
|
||||||
scope: self.scope.clone(),
|
scope: self.scope.clone(),
|
||||||
delegation_scope: self.delegation_scope.clone(),
|
delegation_scope: self.delegation_scope.clone(),
|
||||||
hook_builder: HookRegistryBuilder::new(),
|
hook_builder: HookRegistryBuilder::new(),
|
||||||
|
|
@ -602,6 +606,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
store,
|
store,
|
||||||
pod_metadata_writer: None,
|
pod_metadata_writer: None,
|
||||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||||
|
workspace_root: pwd.clone(),
|
||||||
pwd,
|
pwd,
|
||||||
scope: SharedScope::new(scope),
|
scope: SharedScope::new(scope),
|
||||||
delegation_scope,
|
delegation_scope,
|
||||||
|
|
@ -712,11 +717,17 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
self.runtime_ticket_role = role;
|
self.runtime_ticket_role = role;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Pod's working directory.
|
/// The Pod's tool/process working directory.
|
||||||
pub fn pwd(&self) -> &Path {
|
pub fn pwd(&self) -> &Path {
|
||||||
&self.pwd
|
&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
|
/// The Pod's directory scope, as a shared atomically-swappable
|
||||||
/// handle. Clone it to share scope state with another consumer
|
/// handle. Clone it to share scope state with another consumer
|
||||||
/// (e.g. a tool that needs to mutate scope dynamically).
|
/// (e.g. a tool that needs to mutate scope dynamically).
|
||||||
|
|
@ -1256,7 +1267,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
.map(|d| d.name)
|
.map(|d| d.name)
|
||||||
.collect()
|
.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 {
|
for warning in agents_md_read.warnings {
|
||||||
if let Some(n) = alerter.as_ref() {
|
if let Some(n) = alerter.as_ref() {
|
||||||
n.alert(AlertLevel::Warn, AlertSource::AgentsMd, warning);
|
n.alert(AlertLevel::Warn, AlertSource::AgentsMd, warning);
|
||||||
|
|
@ -2801,7 +2812,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
// `Some(0)` means disabled, same as `None`. Otherwise the
|
// `Some(0)` means disabled, same as `None`. Otherwise the
|
||||||
// `tokens_since >= 0` comparison would fire on every post-run.
|
// `tokens_since >= 0` comparison would fire on every post-run.
|
||||||
let Some(threshold) = memory_cfg.extract_threshold.filter(|n| *n > 0) else {
|
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
|
let model = memory_cfg
|
||||||
.extract_model
|
.extract_model
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -2831,7 +2842,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||||
.is_err()
|
.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
|
let model = memory_cfg
|
||||||
.extract_model
|
.extract_model
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -2887,7 +2898,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
) -> Result<ExtractDecision, PodError> {
|
) -> Result<ExtractDecision, PodError> {
|
||||||
use memory::extract;
|
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
|
let model = memory_cfg
|
||||||
.extract_model
|
.extract_model
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -3210,7 +3221,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
let files_threshold = memory_cfg.consolidation_threshold_files.filter(|n| *n > 0);
|
let files_threshold = memory_cfg.consolidation_threshold_files.filter(|n| *n > 0);
|
||||||
let bytes_threshold = memory_cfg.consolidation_threshold_bytes.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() {
|
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
|
let model = memory_cfg
|
||||||
.consolidation_model
|
.consolidation_model
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -3238,7 +3249,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||||
.is_err()
|
.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
|
let model = memory_cfg
|
||||||
.consolidation_model
|
.consolidation_model
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -3291,7 +3302,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
) -> Result<ConsolidateDecision, PodError> {
|
) -> Result<ConsolidateDecision, PodError> {
|
||||||
use memory::consolidate;
|
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
|
let model = memory_cfg
|
||||||
.consolidation_model
|
.consolidation_model
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -3747,6 +3758,7 @@ where
|
||||||
pod_metadata_writer,
|
pod_metadata_writer,
|
||||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||||
pwd: common.pwd,
|
pwd: common.pwd,
|
||||||
|
workspace_root: common.workspace_root,
|
||||||
scope: SharedScope::new(common.scope),
|
scope: SharedScope::new(common.scope),
|
||||||
delegation_scope: common.delegation_scope,
|
delegation_scope: common.delegation_scope,
|
||||||
hook_builder: HookRegistryBuilder::new(),
|
hook_builder: HookRegistryBuilder::new(),
|
||||||
|
|
@ -3802,7 +3814,34 @@ where
|
||||||
loader: PromptLoader,
|
loader: PromptLoader,
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
) -> Result<Self, PodError> {
|
) -> 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);
|
let skill_shadows = std::mem::take(&mut common.skill_shadows);
|
||||||
|
|
||||||
// A spawned child starts its own conversation, so it mints a
|
// A spawned child starts its own conversation, so it mints a
|
||||||
|
|
@ -3827,6 +3866,7 @@ where
|
||||||
pod_metadata_writer,
|
pod_metadata_writer,
|
||||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||||
pwd: common.pwd,
|
pwd: common.pwd,
|
||||||
|
workspace_root: common.workspace_root,
|
||||||
scope: SharedScope::new(common.scope),
|
scope: SharedScope::new(common.scope),
|
||||||
delegation_scope: common.delegation_scope,
|
delegation_scope: common.delegation_scope,
|
||||||
hook_builder: HookRegistryBuilder::new(),
|
hook_builder: HookRegistryBuilder::new(),
|
||||||
|
|
@ -4006,6 +4046,7 @@ where
|
||||||
pod_metadata_writer,
|
pod_metadata_writer,
|
||||||
segment_state: SegmentState::new(session_id, segment_id, state.entries_count),
|
segment_state: SegmentState::new(session_id, segment_id, state.entries_count),
|
||||||
pwd: common.pwd,
|
pwd: common.pwd,
|
||||||
|
workspace_root: common.workspace_root,
|
||||||
scope: SharedScope::new(common.scope),
|
scope: SharedScope::new(common.scope),
|
||||||
delegation_scope: common.delegation_scope,
|
delegation_scope: common.delegation_scope,
|
||||||
hook_builder: HookRegistryBuilder::new(),
|
hook_builder: HookRegistryBuilder::new(),
|
||||||
|
|
@ -4630,11 +4671,13 @@ pub enum PodError {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bundle of resources that every high-level Pod constructor needs:
|
/// Bundle of resources that every high-level Pod constructor needs:
|
||||||
/// pwd, scope, an LLM client, the prompt catalog, and (optionally) a
|
/// tool pwd, runtime workspace root, scope, an LLM client, the prompt catalog,
|
||||||
/// parsed system-prompt template. Built once by [`prepare_pod_common`]
|
/// and (optionally) a parsed system-prompt template. Built once by
|
||||||
/// from the resolved manifest and then split into Pod fields.
|
/// [`prepare_pod_common`] from the resolved manifest and then split into Pod
|
||||||
|
/// fields.
|
||||||
struct PodCommon {
|
struct PodCommon {
|
||||||
pwd: PathBuf,
|
pwd: PathBuf,
|
||||||
|
workspace_root: PathBuf,
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
delegation_scope: DelegationScope,
|
delegation_scope: DelegationScope,
|
||||||
client: Box<dyn LlmClient>,
|
client: Box<dyn LlmClient>,
|
||||||
|
|
@ -4725,8 +4768,9 @@ fn prepare_pod_common(
|
||||||
parse_template: bool,
|
parse_template: bool,
|
||||||
) -> Result<PodCommon, PodError> {
|
) -> Result<PodCommon, PodError> {
|
||||||
let pwd = current_pwd()?;
|
let pwd = current_pwd()?;
|
||||||
let scope = build_scope_with_memory(manifest, &pwd)?;
|
let workspace_root = pwd.clone();
|
||||||
prepare_pod_common_from_scope(manifest, loader, parse_template, pwd, scope)
|
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(
|
fn prepare_pod_common_with_scope(
|
||||||
|
|
@ -4736,17 +4780,45 @@ fn prepare_pod_common_with_scope(
|
||||||
scope_config: ScopeConfig,
|
scope_config: ScopeConfig,
|
||||||
) -> Result<PodCommon, PodError> {
|
) -> Result<PodCommon, PodError> {
|
||||||
let pwd = current_pwd()?;
|
let pwd = current_pwd()?;
|
||||||
|
let workspace_root = pwd.clone();
|
||||||
let scope = Scope::from_config(&scope_config).map_err(PodError::Scope)?;
|
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(
|
fn prepare_pod_common_from_scope(
|
||||||
manifest: &PodManifest,
|
manifest: &PodManifest,
|
||||||
loader: &PromptLoader,
|
loader: &PromptLoader,
|
||||||
parse_template: bool,
|
parse_template: bool,
|
||||||
|
workspace_root: PathBuf,
|
||||||
pwd: PathBuf,
|
pwd: PathBuf,
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
) -> Result<PodCommon, PodError> {
|
) -> Result<PodCommon, PodError> {
|
||||||
|
if !scope.is_readable(&workspace_root) {
|
||||||
|
return Err(PodError::PwdOutsideScope {
|
||||||
|
pwd: workspace_root,
|
||||||
|
});
|
||||||
|
}
|
||||||
if !scope.is_readable(&pwd) {
|
if !scope.is_readable(&pwd) {
|
||||||
return Err(PodError::PwdOutsideScope { pwd });
|
return Err(PodError::PwdOutsideScope { pwd });
|
||||||
}
|
}
|
||||||
|
|
@ -4758,7 +4830,7 @@ fn prepare_pod_common_from_scope(
|
||||||
let memory_layout = manifest
|
let memory_layout = manifest
|
||||||
.memory
|
.memory
|
||||||
.as_ref()
|
.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() {
|
let mut workflow_registry = match memory_layout.as_ref() {
|
||||||
Some(layout) => workflow_crate::load_workflows(layout).map_err(PodError::WorkflowLoad)?,
|
Some(layout) => workflow_crate::load_workflows(layout).map_err(PodError::WorkflowLoad)?,
|
||||||
None => workflow_crate::WorkflowRegistry::empty(),
|
None => workflow_crate::WorkflowRegistry::empty(),
|
||||||
|
|
@ -4776,6 +4848,7 @@ fn prepare_pod_common_from_scope(
|
||||||
|
|
||||||
Ok(PodCommon {
|
Ok(PodCommon {
|
||||||
pwd,
|
pwd,
|
||||||
|
workspace_root,
|
||||||
scope,
|
scope,
|
||||||
delegation_scope,
|
delegation_scope,
|
||||||
client,
|
client,
|
||||||
|
|
@ -4881,6 +4954,70 @@ fn current_pwd() -> Result<PathBuf, PodError> {
|
||||||
.map_err(|source| PodError::InvalidPwd { pwd: cwd, source })
|
.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)]
|
#[cfg(test)]
|
||||||
mod memory_worker_event_tests {
|
mod memory_worker_event_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ use manifest::{
|
||||||
CompactionConfigPartial, DelegationScope, FileUploadLimitsPartial, Permission,
|
CompactionConfigPartial, DelegationScope, FileUploadLimitsPartial, Permission,
|
||||||
PermissionConfigPartial, PodManifest, PodManifestConfig, PodMetaConfig, ProfileDiscovery,
|
PermissionConfigPartial, PodManifest, PodManifestConfig, PodMetaConfig, ProfileDiscovery,
|
||||||
ProfileError, ProfileRegistry, ProfileRegistrySource, ProfileResolveOptions, ProfileResolver,
|
ProfileError, ProfileRegistry, ProfileRegistrySource, ProfileResolveOptions, ProfileResolver,
|
||||||
ProfileSelector, ScopeConfig, ScopeRule, SessionConfigPartial, SharedScope,
|
ProfileSelector, Scope, ScopeConfig, ScopeRule, SessionConfigPartial, SharedScope,
|
||||||
ToolOutputLimitsPartial, WorkerManifestConfig,
|
ToolOutputLimitsPartial, WorkerManifestConfig,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -52,6 +52,11 @@ struct SpawnPodInput {
|
||||||
/// Instruction-file reference (e.g. `$yoi/default`, `$user/my-agent`).
|
/// Instruction-file reference (e.g. `$yoi/default`, `$user/my-agent`).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
instruction: Option<String>,
|
instruction: Option<String>,
|
||||||
|
/// Child process/tool working directory. This is not the runtime workspace
|
||||||
|
/// root and grants no filesystem authority. When omitted, the spawned Pod
|
||||||
|
/// starts in the spawner's current working directory.
|
||||||
|
#[serde(default)]
|
||||||
|
cwd: Option<PathBuf>,
|
||||||
/// First message sent to the spawned Pod via `Method::Run`.
|
/// First message sent to the spawned Pod via `Method::Run`.
|
||||||
task: String,
|
task: String,
|
||||||
/// Allow rules delegated to the spawned Pod. Must be a subset of the
|
/// Allow rules delegated to the spawned Pod. Must be a subset of the
|
||||||
|
|
@ -219,8 +224,11 @@ pub struct SpawnPodTool {
|
||||||
/// Root of the `$XDG_RUNTIME_DIR/yoi/` tree, used to predict
|
/// Root of the `$XDG_RUNTIME_DIR/yoi/` tree, used to predict
|
||||||
/// the spawned Pod's socket path before the child has bound it.
|
/// the spawned Pod's socket path before the child has bound it.
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
/// Directory the spawned Pod should run in when the LLM did not
|
/// Inherited runtime workspace root for Profile/project/Ticket/workflow/
|
||||||
/// override it. Defaults to the spawner's pwd — see module docs.
|
/// 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,
|
spawner_pwd: PathBuf,
|
||||||
/// Optional typed runtime command injected by tests. Production resolves
|
/// Optional typed runtime command injected by tests. Production resolves
|
||||||
/// the runtime command from `std::env::current_exe()` at launch time.
|
/// the runtime command from `std::env::current_exe()` at launch time.
|
||||||
|
|
@ -261,6 +269,7 @@ impl SpawnPodTool {
|
||||||
spawner_name: String,
|
spawner_name: String,
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
|
workspace_root: PathBuf,
|
||||||
spawner_pwd: PathBuf,
|
spawner_pwd: PathBuf,
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
registry: Arc<SpawnedPodRegistry>,
|
||||||
parent_socket: Option<PathBuf>,
|
parent_socket: Option<PathBuf>,
|
||||||
|
|
@ -274,6 +283,7 @@ impl SpawnPodTool {
|
||||||
spawner_name,
|
spawner_name,
|
||||||
callback_socket,
|
callback_socket,
|
||||||
runtime_base,
|
runtime_base,
|
||||||
|
workspace_root,
|
||||||
spawner_pwd,
|
spawner_pwd,
|
||||||
runtime_command,
|
runtime_command,
|
||||||
registry,
|
registry,
|
||||||
|
|
@ -304,6 +314,7 @@ impl Tool for SpawnPodTool {
|
||||||
|
|
||||||
let scope_allow = parse_scope(&input.scope)?;
|
let scope_allow = parse_scope(&input.scope)?;
|
||||||
self.validate_delegation_scope(&scope_allow)?;
|
self.validate_delegation_scope(&scope_allow)?;
|
||||||
|
let child_cwd = validate_spawn_cwd(input.cwd.as_deref(), &scope_allow, &self.spawner_pwd)?;
|
||||||
|
|
||||||
let spawn_selector =
|
let spawn_selector =
|
||||||
parse_spawn_profile_selector(input.profile.as_deref()).map_err(|msg| {
|
parse_spawn_profile_selector(input.profile.as_deref()).map_err(|msg| {
|
||||||
|
|
@ -349,7 +360,12 @@ impl Tool for SpawnPodTool {
|
||||||
// entry on exit.
|
// entry on exit.
|
||||||
|
|
||||||
let start_outcome = self
|
let start_outcome = self
|
||||||
.exec_child(&input.name, &spawn_config_json, &predicted_socket)
|
.exec_child(
|
||||||
|
&input.name,
|
||||||
|
&spawn_config_json,
|
||||||
|
&predicted_socket,
|
||||||
|
&child_cwd,
|
||||||
|
)
|
||||||
.await;
|
.await;
|
||||||
if let Err(e) = start_outcome {
|
if let Err(e) = start_outcome {
|
||||||
self.release_reservation(&lock_path, &input.name);
|
self.release_reservation(&lock_path, &input.name);
|
||||||
|
|
@ -422,6 +438,7 @@ impl SpawnPodTool {
|
||||||
pod_name: &str,
|
pod_name: &str,
|
||||||
spawn_config_json: &str,
|
spawn_config_json: &str,
|
||||||
predicted_socket: &Path,
|
predicted_socket: &Path,
|
||||||
|
child_cwd: &Path,
|
||||||
) -> Result<(), ToolError> {
|
) -> Result<(), ToolError> {
|
||||||
let runtime_command = match &self.runtime_command {
|
let runtime_command = match &self.runtime_command {
|
||||||
Some(command) => command.clone(),
|
Some(command) => command.clone(),
|
||||||
|
|
@ -458,7 +475,11 @@ impl SpawnPodTool {
|
||||||
.arg(&self.callback_socket)
|
.arg(&self.callback_socket)
|
||||||
.arg("--spawn-config-json")
|
.arg("--spawn-config-json")
|
||||||
.arg(spawn_config_json)
|
.arg(spawn_config_json)
|
||||||
.current_dir(&self.spawner_pwd)
|
.arg("--workspace")
|
||||||
|
.arg(&self.workspace_root)
|
||||||
|
.arg("--tool-cwd")
|
||||||
|
.arg(child_cwd)
|
||||||
|
.current_dir(&self.workspace_root)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::from(stderr_file))
|
.stderr(Stdio::from(stderr_file))
|
||||||
|
|
@ -531,14 +552,67 @@ fn parse_scope(rules: &[ScopeRuleInput]) -> Result<Vec<ScopeRule>, ToolError> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_spawn_cwd(
|
||||||
|
cwd: Option<&Path>,
|
||||||
|
scope_allow: &[ScopeRule],
|
||||||
|
default_cwd: &Path,
|
||||||
|
) -> Result<PathBuf, ToolError> {
|
||||||
|
let Some(cwd) = cwd else {
|
||||||
|
return Ok(default_cwd.to_path_buf());
|
||||||
|
};
|
||||||
|
if !cwd.is_absolute() {
|
||||||
|
return Err(ToolError::InvalidArgument(format!(
|
||||||
|
"SpawnPod.cwd must be absolute: {}",
|
||||||
|
cwd.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let metadata = std::fs::metadata(cwd).map_err(|e| {
|
||||||
|
if e.kind() == std::io::ErrorKind::NotFound {
|
||||||
|
ToolError::InvalidArgument(format!("SpawnPod.cwd does not exist: {}", cwd.display()))
|
||||||
|
} else {
|
||||||
|
ToolError::InvalidArgument(format!(
|
||||||
|
"SpawnPod.cwd is not usable: {}: {e}",
|
||||||
|
cwd.display()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
if !metadata.is_dir() {
|
||||||
|
return Err(ToolError::InvalidArgument(format!(
|
||||||
|
"SpawnPod.cwd must be a directory: {}",
|
||||||
|
cwd.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let canonical = std::fs::canonicalize(cwd).map_err(|e| {
|
||||||
|
ToolError::InvalidArgument(format!(
|
||||||
|
"SpawnPod.cwd is not usable: {}: {e}",
|
||||||
|
cwd.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let child_scope = Scope::from_config(&ScopeConfig {
|
||||||
|
allow: scope_allow.to_vec(),
|
||||||
|
deny: Vec::new(),
|
||||||
|
})
|
||||||
|
.map_err(|e| {
|
||||||
|
ToolError::InvalidArgument(format!(
|
||||||
|
"requested child scope cannot validate SpawnPod.cwd: {e}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if !child_scope.is_readable(&canonical) {
|
||||||
|
return Err(ToolError::InvalidArgument(format!(
|
||||||
|
"SpawnPod.cwd {} is outside the child's delegated readable scope; cwd grants no authority, so add an explicit read or write scope rule covering it",
|
||||||
|
cwd.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(canonical)
|
||||||
|
}
|
||||||
|
|
||||||
/// Serialise the internal manifest config that gets handed to the child
|
/// Serialise the internal manifest config that gets handed to the child
|
||||||
/// Pod runtime process via the hidden `--spawn-config-json` flag.
|
/// Pod runtime process via the hidden `--spawn-config-json` flag.
|
||||||
/// `PodManifestConfig`'s `Serialize` impl is the single source of truth for the
|
/// `PodManifestConfig`'s `Serialize` impl is the single source of truth for the
|
||||||
/// internal handoff shape.
|
/// internal handoff shape.
|
||||||
///
|
///
|
||||||
/// The child's working directory is set separately via
|
/// The child's tool working directory is carried separately through
|
||||||
/// `Command::current_dir` (see [`SpawnPodTool::exec_child`]) — it is
|
/// the child runtime entrypoint; it is not part of the manifest.
|
||||||
/// not part of the manifest.
|
|
||||||
impl SpawnPodTool {
|
impl SpawnPodTool {
|
||||||
fn build_spawn_config_json(
|
fn build_spawn_config_json(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -550,7 +624,7 @@ impl SpawnPodTool {
|
||||||
build_spawn_config_json_for_profile(
|
build_spawn_config_json_for_profile(
|
||||||
&self.spawner_manifest,
|
&self.spawner_manifest,
|
||||||
&self.available_profiles,
|
&self.available_profiles,
|
||||||
&self.spawner_pwd,
|
&self.workspace_root,
|
||||||
name,
|
name,
|
||||||
instruction_override,
|
instruction_override,
|
||||||
scope_allow,
|
scope_allow,
|
||||||
|
|
@ -562,7 +636,7 @@ impl SpawnPodTool {
|
||||||
fn build_spawn_config_json_for_profile(
|
fn build_spawn_config_json_for_profile(
|
||||||
spawner_manifest: &PodManifest,
|
spawner_manifest: &PodManifest,
|
||||||
available_profiles: &AvailableProfiles,
|
available_profiles: &AvailableProfiles,
|
||||||
spawner_pwd: &Path,
|
workspace_root: &Path,
|
||||||
name: &str,
|
name: &str,
|
||||||
instruction_override: Option<&str>,
|
instruction_override: Option<&str>,
|
||||||
scope_allow: &[ScopeRule],
|
scope_allow: &[ScopeRule],
|
||||||
|
|
@ -584,7 +658,7 @@ fn build_spawn_config_json_for_profile(
|
||||||
SpawnProfileSelector::Inherit => unreachable!(),
|
SpawnProfileSelector::Inherit => unreachable!(),
|
||||||
};
|
};
|
||||||
let resolved = ProfileResolver::new()
|
let resolved = ProfileResolver::new()
|
||||||
.with_workspace_base(spawner_pwd)
|
.with_workspace_base(workspace_root)
|
||||||
.resolve_from_registry(
|
.resolve_from_registry(
|
||||||
&profile_selector,
|
&profile_selector,
|
||||||
registry,
|
registry,
|
||||||
|
|
@ -801,6 +875,7 @@ pub fn spawn_pod_tool(
|
||||||
spawner_name: String,
|
spawner_name: String,
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
|
workspace_root: PathBuf,
|
||||||
spawner_pwd: PathBuf,
|
spawner_pwd: PathBuf,
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
registry: Arc<SpawnedPodRegistry>,
|
||||||
parent_socket: Option<PathBuf>,
|
parent_socket: Option<PathBuf>,
|
||||||
|
|
@ -812,6 +887,7 @@ pub fn spawn_pod_tool(
|
||||||
spawner_name,
|
spawner_name,
|
||||||
callback_socket,
|
callback_socket,
|
||||||
runtime_base,
|
runtime_base,
|
||||||
|
workspace_root,
|
||||||
spawner_pwd,
|
spawner_pwd,
|
||||||
registry,
|
registry,
|
||||||
parent_socket,
|
parent_socket,
|
||||||
|
|
@ -827,6 +903,7 @@ pub fn spawn_pod_tool_with_runtime_command(
|
||||||
spawner_name: String,
|
spawner_name: String,
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
|
workspace_root: PathBuf,
|
||||||
spawner_pwd: PathBuf,
|
spawner_pwd: PathBuf,
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
registry: Arc<SpawnedPodRegistry>,
|
||||||
parent_socket: Option<PathBuf>,
|
parent_socket: Option<PathBuf>,
|
||||||
|
|
@ -839,6 +916,7 @@ pub fn spawn_pod_tool_with_runtime_command(
|
||||||
spawner_name,
|
spawner_name,
|
||||||
callback_socket,
|
callback_socket,
|
||||||
runtime_base,
|
runtime_base,
|
||||||
|
workspace_root,
|
||||||
spawner_pwd,
|
spawner_pwd,
|
||||||
registry,
|
registry,
|
||||||
parent_socket,
|
parent_socket,
|
||||||
|
|
@ -853,6 +931,7 @@ fn spawn_pod_tool_impl(
|
||||||
spawner_name: String,
|
spawner_name: String,
|
||||||
callback_socket: PathBuf,
|
callback_socket: PathBuf,
|
||||||
runtime_base: PathBuf,
|
runtime_base: PathBuf,
|
||||||
|
workspace_root: PathBuf,
|
||||||
spawner_pwd: PathBuf,
|
spawner_pwd: PathBuf,
|
||||||
registry: Arc<SpawnedPodRegistry>,
|
registry: Arc<SpawnedPodRegistry>,
|
||||||
parent_socket: Option<PathBuf>,
|
parent_socket: Option<PathBuf>,
|
||||||
|
|
@ -864,7 +943,7 @@ fn spawn_pod_tool_impl(
|
||||||
Arc::new(move || {
|
Arc::new(move || {
|
||||||
let schema = schemars::schema_for!(SpawnPodInput);
|
let schema = schemars::schema_for!(SpawnPodInput);
|
||||||
let schema_value = serde_json::to_value(schema).unwrap_or(serde_json::json!({}));
|
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
|
let description = prompts
|
||||||
.spawn_pod_tool_description(
|
.spawn_pod_tool_description(
|
||||||
&available_profiles.compact_list(),
|
&available_profiles.compact_list(),
|
||||||
|
|
@ -884,6 +963,7 @@ fn spawn_pod_tool_impl(
|
||||||
spawner_name.clone(),
|
spawner_name.clone(),
|
||||||
callback_socket.clone(),
|
callback_socket.clone(),
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
|
workspace_root.clone(),
|
||||||
spawner_pwd.clone(),
|
spawner_pwd.clone(),
|
||||||
registry.clone(),
|
registry.clone(),
|
||||||
parent_socket.clone(),
|
parent_socket.clone(),
|
||||||
|
|
@ -912,6 +992,63 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_pod_input_schema_includes_optional_cwd() {
|
||||||
|
let schema = serde_json::to_value(schemars::schema_for!(SpawnPodInput)).unwrap();
|
||||||
|
let properties = schema
|
||||||
|
.get("properties")
|
||||||
|
.and_then(serde_json::Value::as_object)
|
||||||
|
.expect("schema properties");
|
||||||
|
assert!(properties.contains_key("cwd"), "schema: {schema}");
|
||||||
|
let required = schema
|
||||||
|
.get("required")
|
||||||
|
.and_then(serde_json::Value::as_array)
|
||||||
|
.expect("schema required list");
|
||||||
|
assert!(
|
||||||
|
!required.iter().any(|value| value.as_str() == Some("cwd")),
|
||||||
|
"cwd must remain optional: {schema}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_pod_validate_cwd_requires_absolute_existing_directory_in_child_scope() {
|
||||||
|
let root = TempDir::new().unwrap();
|
||||||
|
let child_cwd = root.path().join("child");
|
||||||
|
std::fs::create_dir(&child_cwd).unwrap();
|
||||||
|
let file_path = root.path().join("file.txt");
|
||||||
|
std::fs::write(&file_path, "not a dir").unwrap();
|
||||||
|
let outside = TempDir::new().unwrap();
|
||||||
|
let missing = root.path().join("missing");
|
||||||
|
let rules = vec![abs_rule(root.path(), Permission::Write)];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
validate_spawn_cwd(None, &rules, root.path()).unwrap(),
|
||||||
|
root.path()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
validate_spawn_cwd(Some(&child_cwd), &rules, root.path()).unwrap(),
|
||||||
|
std::fs::canonicalize(&child_cwd).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
for (cwd, expected) in [
|
||||||
|
(Path::new("relative"), "must be absolute"),
|
||||||
|
(missing.as_path(), "does not exist"),
|
||||||
|
(file_path.as_path(), "must be a directory"),
|
||||||
|
(
|
||||||
|
outside.path(),
|
||||||
|
"outside the child's delegated readable scope",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let err = validate_spawn_cwd(Some(cwd), &rules, root.path()).unwrap_err();
|
||||||
|
match err {
|
||||||
|
ToolError::InvalidArgument(message) => {
|
||||||
|
assert!(message.contains(expected), "{message}")
|
||||||
|
}
|
||||||
|
other => panic!("expected InvalidArgument, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parent_manifest(root: &Path, deny: Option<&Path>) -> PodManifest {
|
fn parent_manifest(root: &Path, deny: Option<&Path>) -> PodManifest {
|
||||||
PodManifestConfig {
|
PodManifestConfig {
|
||||||
pod: PodMetaConfig {
|
pod: PodMetaConfig {
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,31 @@ fn mock_runtime_command() -> PodRuntimeCommand {
|
||||||
PodRuntimeCommand::new(which_true(), Vec::new())
|
PodRuntimeCommand::new(which_true(), Vec::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cwd_recording_runtime_command(script_path: &Path, output_path: &Path) -> PodRuntimeCommand {
|
||||||
|
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_runtime_invocation(output_path: &Path) -> Vec<String> {
|
||||||
|
for _ in 0..50 {
|
||||||
|
if let Ok(content) = std::fs::read_to_string(output_path) {
|
||||||
|
return content.lines().map(str::to_owned).collect();
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
panic!(
|
||||||
|
"runtime command did not record invocation at {}",
|
||||||
|
output_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// `/bin/true` only exists on FHS-compliant systems. Resolve it via PATH
|
/// `/bin/true` only exists on FHS-compliant systems. Resolve it via PATH
|
||||||
/// so the tests work regardless of distro.
|
/// so the tests work regardless of distro.
|
||||||
fn which_true() -> String {
|
fn which_true() -> String {
|
||||||
|
|
@ -160,6 +185,19 @@ fn which_true() -> String {
|
||||||
"/bin/true".into()
|
"/bin/true".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn which_sh() -> String {
|
||||||
|
for dir in std::env::var_os("PATH")
|
||||||
|
.map(|p| std::env::split_paths(&p).collect::<Vec<_>>())
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
let candidate = dir.join("sh");
|
||||||
|
if candidate.is_file() {
|
||||||
|
return candidate.to_string_lossy().into_owned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"/bin/sh".into()
|
||||||
|
}
|
||||||
|
|
||||||
/// Tests don't exercise the model — they intercept the spawned
|
/// Tests don't exercise the model — they intercept the spawned
|
||||||
/// child via a mock socket — but `spawn_pod_tool` needs a value to
|
/// child via a mock socket — but `spawn_pod_tool` needs a value to
|
||||||
/// embed in the overlay TOML. Any well-formed `ModelManifest` works.
|
/// embed in the overlay TOML. Any well-formed `ModelManifest` works.
|
||||||
|
|
@ -231,6 +269,124 @@ fn clear_env() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn spawn_pod_launches_runtime_in_workspace_and_passes_tool_cwd() {
|
||||||
|
let _env = EnvGuard::acquire();
|
||||||
|
|
||||||
|
let allow_root = TempDir::new().unwrap();
|
||||||
|
let child_cwd = allow_root.path().join("child-cwd");
|
||||||
|
std::fs::create_dir(&child_cwd).unwrap();
|
||||||
|
let script = allow_root.path().join("record-pwd.sh");
|
||||||
|
let output_path = allow_root.path().join("pwd.txt");
|
||||||
|
let (_tmp, runtime_base, spawner_socket, spawner_rd) =
|
||||||
|
setup_spawner("root", allow_root.path()).await;
|
||||||
|
|
||||||
|
let (_predicted_socket, listener) = bind_mock_pod_socket(&runtime_base, "child-cwd").await;
|
||||||
|
let received = accept_one_method(listener);
|
||||||
|
|
||||||
|
let registry = SpawnedPodRegistry::new(spawner_rd);
|
||||||
|
let def = spawn_pod_tool_with_runtime_command(
|
||||||
|
"root".into(),
|
||||||
|
spawner_socket,
|
||||||
|
runtime_base,
|
||||||
|
allow_root.path().to_path_buf(),
|
||||||
|
allow_root.path().to_path_buf(),
|
||||||
|
registry,
|
||||||
|
None,
|
||||||
|
dummy_manifest(allow_root.path()),
|
||||||
|
shared_scope_for(allow_root.path()),
|
||||||
|
builtin_prompts(),
|
||||||
|
cwd_recording_runtime_command(&script, &output_path),
|
||||||
|
);
|
||||||
|
let (_meta, tool) = def();
|
||||||
|
|
||||||
|
let input = json!({
|
||||||
|
"name": "child-cwd",
|
||||||
|
"task": "hello",
|
||||||
|
"profile": "inherit",
|
||||||
|
"cwd": child_cwd.to_str().unwrap(),
|
||||||
|
"scope": [{
|
||||||
|
"target": allow_root.path().to_str().unwrap(),
|
||||||
|
"permission": "write"
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
tool.execute(&input).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!(
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn spawn_pod_omitted_cwd_preserves_spawner_pwd() {
|
||||||
|
let _env = EnvGuard::acquire();
|
||||||
|
|
||||||
|
let allow_root = TempDir::new().unwrap();
|
||||||
|
let script = allow_root.path().join("record-pwd.sh");
|
||||||
|
let output_path = allow_root.path().join("pwd.txt");
|
||||||
|
let (_tmp, runtime_base, spawner_socket, spawner_rd) =
|
||||||
|
setup_spawner("root", allow_root.path()).await;
|
||||||
|
|
||||||
|
let (_predicted_socket, listener) =
|
||||||
|
bind_mock_pod_socket(&runtime_base, "child-default-cwd").await;
|
||||||
|
let received = accept_one_method(listener);
|
||||||
|
|
||||||
|
let registry = SpawnedPodRegistry::new(spawner_rd);
|
||||||
|
let def = spawn_pod_tool_with_runtime_command(
|
||||||
|
"root".into(),
|
||||||
|
spawner_socket,
|
||||||
|
runtime_base,
|
||||||
|
allow_root.path().to_path_buf(),
|
||||||
|
allow_root.path().to_path_buf(),
|
||||||
|
registry,
|
||||||
|
None,
|
||||||
|
dummy_manifest(allow_root.path()),
|
||||||
|
shared_scope_for(allow_root.path()),
|
||||||
|
builtin_prompts(),
|
||||||
|
cwd_recording_runtime_command(&script, &output_path),
|
||||||
|
);
|
||||||
|
let (_meta, tool) = def();
|
||||||
|
|
||||||
|
let input = json!({
|
||||||
|
"name": "child-default-cwd",
|
||||||
|
"task": "hello",
|
||||||
|
"profile": "inherit",
|
||||||
|
"scope": [{
|
||||||
|
"target": allow_root.path().to_str().unwrap(),
|
||||||
|
"permission": "write"
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
tool.execute(&input).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!(
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn spawn_pod_delegates_scope_and_sends_run() {
|
async fn spawn_pod_delegates_scope_and_sends_run() {
|
||||||
let _env = EnvGuard::acquire();
|
let _env = EnvGuard::acquire();
|
||||||
|
|
@ -249,6 +405,7 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
|
||||||
spawner_socket.clone(),
|
spawner_socket.clone(),
|
||||||
runtime_base.clone(),
|
runtime_base.clone(),
|
||||||
allow_root.path().to_path_buf(),
|
allow_root.path().to_path_buf(),
|
||||||
|
allow_root.path().to_path_buf(),
|
||||||
registry,
|
registry,
|
||||||
None,
|
None,
|
||||||
dummy_manifest(allow_root.path()),
|
dummy_manifest(allow_root.path()),
|
||||||
|
|
@ -341,6 +498,7 @@ async fn spawn_pod_requires_explicit_delegation_even_with_direct_scope() {
|
||||||
spawner_socket,
|
spawner_socket,
|
||||||
runtime_base,
|
runtime_base,
|
||||||
allow_root.path().to_path_buf(),
|
allow_root.path().to_path_buf(),
|
||||||
|
allow_root.path().to_path_buf(),
|
||||||
registry,
|
registry,
|
||||||
None,
|
None,
|
||||||
manifest,
|
manifest,
|
||||||
|
|
@ -407,6 +565,7 @@ async fn spawn_pod_rejects_child_non_recursive_scope_under_parent_non_recursive_
|
||||||
spawner_socket,
|
spawner_socket,
|
||||||
runtime_base,
|
runtime_base,
|
||||||
allow_root.path().to_path_buf(),
|
allow_root.path().to_path_buf(),
|
||||||
|
allow_root.path().to_path_buf(),
|
||||||
registry,
|
registry,
|
||||||
None,
|
None,
|
||||||
manifest,
|
manifest,
|
||||||
|
|
@ -458,6 +617,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
|
||||||
spawner_socket,
|
spawner_socket,
|
||||||
runtime_base,
|
runtime_base,
|
||||||
allow_root.path().to_path_buf(),
|
allow_root.path().to_path_buf(),
|
||||||
|
allow_root.path().to_path_buf(),
|
||||||
registry,
|
registry,
|
||||||
None,
|
None,
|
||||||
dummy_manifest(allow_root.path()),
|
dummy_manifest(allow_root.path()),
|
||||||
|
|
@ -531,6 +691,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
|
||||||
spawner_socket,
|
spawner_socket,
|
||||||
runtime_base,
|
runtime_base,
|
||||||
allow_root.path().to_path_buf(),
|
allow_root.path().to_path_buf(),
|
||||||
|
allow_root.path().to_path_buf(),
|
||||||
registry,
|
registry,
|
||||||
None,
|
None,
|
||||||
dummy_manifest(allow_root.path()),
|
dummy_manifest(allow_root.path()),
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ pod_orchestration_guidance_section = "{% include \"$yoi/common/pod-orchestration
|
||||||
spawn_pod_tool_description = """\
|
spawn_pod_tool_description = """\
|
||||||
Spawn a new Pod process to work on a delegated task. The spawner's write scope is reduced by the scope passed here; the spawned Pod receives its own socket and starts running `task` immediately. The spawned Pod outlives the spawner's current turn and can be contacted again through its socket path.
|
Spawn a new Pod process to work on a delegated task. The spawner's write scope is reduced by the scope passed here; the spawned Pod receives its own socket and starts running `task` immediately. The spawned Pod outlives the spawner's current turn and can be contacted again through its socket path.
|
||||||
|
|
||||||
|
Optional `cwd`: when provided, it is the child process/tool default working directory only. It must be an absolute existing directory covered by the child's delegated readable scope, and it does not change workspace/Profile/memory/Ticket roots or grant authority.
|
||||||
|
|
||||||
Profile selection: `profile` may be omitted or set to `default` to use the effective child default profile, set to `inherit` to derive reusable child configuration from this Pod, or set to one of the registry selectors below. Raw/path profile selectors are not accepted by SpawnPod. `scope` is always the only delegated filesystem capability; profile scope is replaced by the explicit SpawnPod scope.
|
Profile selection: `profile` may be omitted or set to `default` to use the effective child default profile, set to `inherit` to derive reusable child configuration from this Pod, or set to one of the registry selectors below. Raw/path profile selectors are not accepted by SpawnPod. `scope` is always the only delegated filesystem capability; profile scope is replaced by the explicit SpawnPod scope.
|
||||||
|
|
||||||
Default profile: {{ default_profile }}
|
Default profile: {{ default_profile }}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user