feat: add SpawnPod cwd
This commit is contained in:
parent
15cf4a1332
commit
3dd77079f1
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -549,7 +549,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");
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -304,6 +309,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 +355,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 +433,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 +470,7 @@ 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)
|
.current_dir(child_cwd)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::from(stderr_file))
|
.stderr(Stdio::from(stderr_file))
|
||||||
|
|
@ -531,6 +543,60 @@ 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
|
||||||
|
|
@ -912,6 +978,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,30 @@ 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 {
|
||||||
|
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(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_recorded_pwd(output_path: &Path) -> String {
|
||||||
|
for _ in 0..50 {
|
||||||
|
if let Ok(content) = std::fs::read_to_string(output_path) {
|
||||||
|
return content.trim_end().to_string();
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
panic!(
|
||||||
|
"runtime command did not record pwd 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 +184,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 +268,108 @@ fn clear_env() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn spawn_pod_runs_child_process_in_provided_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(),
|
||||||
|
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 { .. })));
|
||||||
|
assert_eq!(
|
||||||
|
read_recorded_pwd(&output_path).await,
|
||||||
|
child_cwd.to_str().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
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(),
|
||||||
|
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 { .. })));
|
||||||
|
assert_eq!(
|
||||||
|
read_recorded_pwd(&output_path).await,
|
||||||
|
allow_root.path().to_str().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
|
||||||
|
|
@ -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