マニフェストを継承してPodをスポーンさせる

This commit is contained in:
Keisuke Hirata 2026-04-19 18:01:47 +09:00
parent 25df7a79c1
commit a89701bc43
5 changed files with 72 additions and 32 deletions

View File

@ -5,7 +5,6 @@
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
- [ ] Pod オーケストレーション
- [ ] 動的 Scope 変更 → [tickets/dynamic-scope.md](tickets/dynamic-scope.md)
- [ ] SpawnPod が親の provider 設定を引き継ぐ → [tickets/spawn-inherit-provider.md](tickets/spawn-inherit-provider.md)
- [ ] ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md)
- [ ] TUI 拡充
- [ ] 新しい Pod を spawn する UI の設計 → [tickets/tui-pod-spawn-ui.md](tickets/tui-pod-spawn-ui.md)

View File

@ -113,6 +113,7 @@ impl PodController {
let scope_for_tools = pod.scope().clone();
let pwd_for_tools = pod.pwd().to_path_buf();
let spawner_name = pod.manifest().pod.name.clone();
let spawner_provider = pod.manifest().provider.clone();
// Parent callback socket (this Pod's own parent, used for
// `PodEvent` upward reports). `None` for top-level Pods.
@ -229,6 +230,7 @@ impl PodController {
pwd_for_tools,
spawned_registry.clone(),
self_parent_socket.clone(),
spawner_provider.clone(),
));
worker.register_tool(send_to_pod_tool(spawned_registry.clone()));
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));

View File

@ -14,7 +14,8 @@ use std::time::Duration;
use async_trait::async_trait;
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use manifest::{
Permission, PodManifestConfig, PodMetaConfig, ScopeConfig, ScopeRule, WorkerManifestConfig,
Permission, PodManifestConfig, PodMetaConfig, ProviderConfig, ProviderConfigPartial,
ScopeConfig, ScopeRule, WorkerManifestConfig,
};
use protocol::Method;
use protocol::stream::JsonLineWriter;
@ -113,6 +114,11 @@ pub struct SpawnPodTool {
/// `None` for top-level Pods — in that case the re-emission is a
/// no-op.
parent_socket: Option<PathBuf>,
/// Spawner's resolved provider config — copied into every spawned
/// Pod's overlay TOML so the child does not need its own provider
/// configuration in the manifest cascade. Per-spawn override is
/// out of scope here (see `tickets/spawn-inherit-provider.md`).
spawner_provider: ProviderConfig,
}
impl SpawnPodTool {
@ -123,6 +129,7 @@ impl SpawnPodTool {
spawner_pwd: PathBuf,
registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>,
spawner_provider: ProviderConfig,
) -> Self {
Self {
spawner_name,
@ -131,6 +138,7 @@ impl SpawnPodTool {
spawner_pwd,
registry,
parent_socket,
spawner_provider,
}
}
}
@ -184,7 +192,12 @@ impl Tool for SpawnPodTool {
// it back — even if later steps (Method::Run delivery, record
// write) fail, the child is running and will release its own
// entry on exit.
let overlay_toml = match build_overlay_toml(&input.name, &instruction, &scope_allow) {
let overlay_toml = match build_overlay_toml(
&input.name,
&instruction,
&scope_allow,
&self.spawner_provider,
) {
Ok(s) => s,
Err(e) => {
self.release_reservation(&lock_path, &input.name);
@ -337,11 +350,18 @@ fn build_overlay_toml(
name: &str,
instruction: &str,
scope_allow: &[ScopeRule],
provider: &ProviderConfig,
) -> Result<String, toml::ser::Error> {
let overlay = PodManifestConfig {
pod: PodMetaConfig {
name: Some(name.to_string()),
},
provider: ProviderConfigPartial {
kind: Some(provider.kind),
model: Some(provider.model.clone()),
api_key_file: provider.api_key_file.clone(),
base_url: provider.base_url.clone(),
},
worker: WorkerManifestConfig {
instruction: Some(instruction.to_string()),
..Default::default()
@ -438,6 +458,7 @@ pub fn spawn_pod_tool(
spawner_pwd: PathBuf,
registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>,
spawner_provider: ProviderConfig,
) -> ToolDefinition {
Arc::new(move || {
let schema = schemars::schema_for!(SpawnPodInput);
@ -452,7 +473,38 @@ pub fn spawn_pod_tool(
spawner_pwd.clone(),
registry.clone(),
parent_socket.clone(),
spawner_provider.clone(),
));
(meta, tool)
})
}
#[cfg(test)]
mod tests {
use super::*;
use manifest::ProviderKind;
#[test]
fn overlay_inherits_spawner_provider() {
let provider = ProviderConfig {
kind: ProviderKind::Anthropic,
model: "claude-sonnet-4".into(),
api_key_file: Some(PathBuf::from("/etc/keys/anthropic")),
base_url: Some("https://example.test".into()),
};
let toml_str = build_overlay_toml("child", "$insomnia/default", &[], &provider).unwrap();
let parsed = PodManifestConfig::from_toml(&toml_str).unwrap();
assert_eq!(parsed.provider.kind, Some(ProviderKind::Anthropic));
assert_eq!(parsed.provider.model.as_deref(), Some("claude-sonnet-4"));
assert_eq!(
parsed.provider.api_key_file.as_deref(),
Some(Path::new("/etc/keys/anthropic"))
);
assert_eq!(
parsed.provider.base_url.as_deref(),
Some("https://example.test")
);
}
}

View File

@ -11,7 +11,7 @@ use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};
use llm_worker::tool::{ToolError, ToolOutput};
use manifest::{Permission, ScopeRule};
use manifest::{Permission, ProviderConfig, ProviderKind, ScopeRule};
use pod::runtime_dir::{RuntimeDir, SpawnedPodRecord};
use pod::scope_lock::{self, LockFileGuard};
use pod::spawn_pod::spawn_pod_tool;
@ -132,6 +132,18 @@ fn which_true() -> String {
"/bin/true".into()
}
/// Tests don't exercise the provider — they intercept the spawned
/// child via a mock socket — but `spawn_pod_tool` needs a value to
/// embed in the overlay TOML. Any well-formed `ProviderConfig` works.
fn dummy_provider() -> ProviderConfig {
ProviderConfig {
kind: ProviderKind::Anthropic,
model: "claude-test".into(),
api_key_file: None,
base_url: None,
}
}
fn clear_env() {
unsafe {
std::env::remove_var("INSOMNIA_SCOPE_LOCK");
@ -159,6 +171,7 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
allow_root.path().to_path_buf(),
registry,
None,
dummy_provider(),
);
let (_meta, tool) = def();
@ -221,6 +234,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
allow_root.path().to_path_buf(),
registry,
None,
dummy_provider(),
);
let (_meta, tool) = def();
@ -279,6 +293,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
allow_root.path().to_path_buf(),
registry,
None,
dummy_provider(),
);
let (_meta, tool) = def();

View File

@ -1,28 +0,0 @@
# SpawnPod が親の provider 設定を引き継ぐ
## 背景
`SpawnPod` ツールは子 Pod に渡す overlay TOML に `pod.name` / `worker.instruction` / `scope` のみを含め、`provider` を含めない(`crates/pod/src/spawn_pod.rs::build_overlay_toml`)。
子 Pod は起動時に manifest cascade を一からやり直すため、デフォルトのインストラクション (`$insomnia/default`) を使うと `provider.kind` が見つからず `failed to resolve manifest cascade: missing required field: provider.kind` で起動失敗する。結果として `SpawnPod` ツールがデフォルト引数では事実上使えない状態になっている。
## ゴール
`SpawnPod` で起動した子 Pod が、特に指定なしで親 Pod と同じ provider 設定kind, model, api_key 等)を使って起動できる。
## 必要な変更
- 親 Pod の resolved provider を `SpawnPodTool` から参照できるようにする(`SpawnPodTool::new` の引数追加 等)
- `build_overlay_toml` が overlay に provider を含める
- ツール入力 (`SpawnPodInput`) からの provider 指定は今回は不要(範囲外を参照)
## 完了条件
- `SpawnPod``provider` 指定なしで呼び出すと、子 Pod が親と同じ provider 設定で起動し、`Method::Run` を受けて応答できる
- 既存の spawn_pod 統合テスト (`crates/pod/tests/spawn_pod_test.rs`) が引き続きパス
- 親が provider を持たないケース(あり得るなら)の挙動が定まっている
## 範囲外
- ツール入力からの provider overrideモデルだけ差し替えたい等のマルチエージェント用途。将来別チケットで `SpawnPodInput` に optional provider override を追加する形で扱う
- インストラクションファイル側で provider を指定する仕組み