feat: dynamic-scopeの実装

This commit is contained in:
2026-05-02 01:26:17 +09:00
parent 5479b14411
commit d33b1c111e
9 changed files with 508 additions and 41 deletions
+46 -1
View File
@@ -11,7 +11,7 @@ use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};
use llm_worker::tool::{ToolError, ToolOutput};
use manifest::{AuthRef, ModelManifest, Permission, SchemeKind, ScopeRule};
use manifest::{AuthRef, ModelManifest, Permission, SchemeKind, Scope, ScopeRule, SharedScope};
use pod::runtime::dir::{RuntimeDir, SpawnedPodRecord};
use pod::runtime::pod_registry::{self, LockFileGuard};
use pod::spawn::registry::SpawnedPodRegistry;
@@ -149,6 +149,14 @@ fn dummy_model() -> ModelManifest {
}
}
/// Spawner-side `SharedScope` mirroring the `allow_root` granted by
/// `setup_spawner`. The tool revokes Write rules from this scope on
/// successful spawn — tests can `load()` it to assert the
/// revocation took effect.
fn shared_scope_for(allow_root: &Path) -> SharedScope {
SharedScope::new(Scope::writable(allow_root).unwrap())
}
fn clear_env() {
unsafe {
std::env::remove_var("INSOMNIA_RUNTIME_DIR");
@@ -169,6 +177,7 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
let received = accept_one_method(listener);
let registry = SpawnedPodRegistry::new(spawner_rd.clone());
let spawner_scope = shared_scope_for(allow_root.path());
let def = spawn_pod_tool(
"root".into(),
spawner_socket.clone(),
@@ -177,6 +186,7 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
registry,
None,
dummy_model(),
spawner_scope.clone(),
);
let (_meta, tool) = def();
@@ -190,6 +200,13 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
})
.to_string();
// Pre-spawn: the spawner can write to the delegated path.
assert!(
spawner_scope
.load()
.is_writable(&allow_root.path().join("a.txt"))
);
let output: ToolOutput = tool.execute(&input).await.unwrap();
assert!(
output.summary.contains("child"),
@@ -225,6 +242,15 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
assert_eq!(records[0].pod_name, "child");
assert_eq!(records[0].callback_address, spawner_socket);
// Post-spawn: the spawner's runtime scope has been demoted on the
// delegated path. Write is gone, Read remains.
let post = spawner_scope.load();
assert_eq!(
post.permission_at(&allow_root.path().join("a.txt")),
Some(Permission::Read),
"spawner should still be able to read delegated path"
);
clear_env();
}
@@ -239,6 +265,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
point_pod_command_at_true();
let registry = SpawnedPodRegistry::new(spawner_rd);
let spawner_scope = shared_scope_for(allow_root.path());
let def = spawn_pod_tool(
"root".into(),
spawner_socket,
@@ -247,6 +274,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
registry,
None,
dummy_model(),
spawner_scope.clone(),
);
let (_meta, tool) = def();
@@ -277,6 +305,13 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
let guard = LockFileGuard::open(&lock_path).unwrap();
assert!(guard.data().find("child").is_none());
// Failed spawn must not have demoted the spawner's scope either.
assert!(
spawner_scope
.load()
.is_writable(&allow_root.path().join("a.txt"))
);
clear_env();
}
@@ -301,6 +336,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
// by running this test alone when iterating.
let registry = SpawnedPodRegistry::new(spawner_rd);
let spawner_scope = shared_scope_for(allow_root.path());
let def = spawn_pod_tool(
"root".into(),
spawner_socket,
@@ -309,6 +345,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
registry,
None,
dummy_model(),
spawner_scope.clone(),
);
let (_meta, tool) = def();
@@ -341,5 +378,13 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
"allocation was not rolled back after socket wait timed out"
);
// Spawner's runtime scope must also be untouched — revoke is
// performed only after exec_child succeeds.
assert!(
spawner_scope
.load()
.is_writable(&allow_root.path().join("a.txt"))
);
clear_env();
}