feat: split direct and delegation scope authority
This commit is contained in:
+16
-2
@@ -22,8 +22,8 @@ use tracing::{info, warn};
|
||||
use crate::segment_log_sink::SegmentLogSink;
|
||||
|
||||
use manifest::{
|
||||
Permission, PodManifest, PodManifestConfig, ResolveError, Scope, ScopeConfig, ScopeError,
|
||||
ScopeRule, SharedScope, WorkerManifest,
|
||||
DelegationScope, Permission, PodManifest, PodManifestConfig, ResolveError, Scope, ScopeConfig,
|
||||
ScopeError, ScopeRule, SharedScope, WorkerManifest,
|
||||
};
|
||||
|
||||
use crate::compact::state::CompactState;
|
||||
@@ -238,6 +238,9 @@ pub struct Pod<C: LlmClient, St: Store> {
|
||||
/// compact worker) so scope updates propagate to every consumer
|
||||
/// at the next permission check.
|
||||
scope: SharedScope,
|
||||
/// Filesystem authority this Pod may pass to spawned children. Direct tools
|
||||
/// continue to use `scope`; SpawnPod validates requested child scope here.
|
||||
delegation_scope: DelegationScope,
|
||||
hook_builder: HookRegistryBuilder,
|
||||
interceptor_installed: bool,
|
||||
/// Shared compaction state (present when threshold is configured).
|
||||
@@ -415,6 +418,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
|
||||
segment_state: self.segment_state.clone(),
|
||||
pwd: self.pwd.clone(),
|
||||
scope: self.scope.clone(),
|
||||
delegation_scope: self.delegation_scope.clone(),
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
interceptor_installed: false,
|
||||
compact_state: None,
|
||||
@@ -585,6 +589,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
let session_id = session_store::new_session_id();
|
||||
let segment_id = session_store::new_segment_id();
|
||||
let prompts = PromptCatalog::builtins_only()?;
|
||||
let delegation_scope =
|
||||
DelegationScope::from_config(&manifest.delegation_scope).map_err(PodError::Scope)?;
|
||||
let mut pod = Self {
|
||||
manifest,
|
||||
worker: Some(worker),
|
||||
@@ -593,6 +599,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||
pwd,
|
||||
scope: SharedScope::new(scope),
|
||||
delegation_scope,
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
interceptor_installed: false,
|
||||
compact_state: None,
|
||||
@@ -3724,6 +3731,7 @@ where
|
||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||
pwd: common.pwd,
|
||||
scope: SharedScope::new(common.scope),
|
||||
delegation_scope: common.delegation_scope,
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
interceptor_installed: false,
|
||||
compact_state: None,
|
||||
@@ -3802,6 +3810,7 @@ where
|
||||
segment_state: SegmentState::new(session_id, segment_id, 0),
|
||||
pwd: common.pwd,
|
||||
scope: SharedScope::new(common.scope),
|
||||
delegation_scope: common.delegation_scope,
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
interceptor_installed: false,
|
||||
compact_state: None,
|
||||
@@ -3979,6 +3988,7 @@ where
|
||||
segment_state: SegmentState::new(session_id, segment_id, state.entries_count),
|
||||
pwd: common.pwd,
|
||||
scope: SharedScope::new(common.scope),
|
||||
delegation_scope: common.delegation_scope,
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
interceptor_installed: false,
|
||||
compact_state: None,
|
||||
@@ -4606,6 +4616,7 @@ pub enum PodError {
|
||||
struct PodCommon {
|
||||
pwd: PathBuf,
|
||||
scope: Scope,
|
||||
delegation_scope: DelegationScope,
|
||||
client: Box<dyn LlmClient>,
|
||||
prompts: Arc<PromptCatalog>,
|
||||
workflow_registry: workflow_crate::WorkflowRegistry,
|
||||
@@ -4719,6 +4730,8 @@ fn prepare_pod_common_from_scope(
|
||||
if !scope.is_readable(&pwd) {
|
||||
return Err(PodError::PwdOutsideScope { pwd });
|
||||
}
|
||||
let delegation_scope =
|
||||
DelegationScope::from_config(&manifest.delegation_scope).map_err(PodError::Scope)?;
|
||||
|
||||
let client = provider::build_client(&manifest.model)?;
|
||||
let prompts = PromptCatalog::load(loader, manifest.pod.prompt_pack.as_deref())?;
|
||||
@@ -4744,6 +4757,7 @@ fn prepare_pod_common_from_scope(
|
||||
Ok(PodCommon {
|
||||
pwd,
|
||||
scope,
|
||||
delegation_scope,
|
||||
client,
|
||||
prompts,
|
||||
workflow_registry,
|
||||
|
||||
@@ -15,10 +15,11 @@ use async_trait::async_trait;
|
||||
use client::PodRuntimeCommand;
|
||||
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use manifest::{
|
||||
CompactionConfigPartial, FileUploadLimitsPartial, Permission, PermissionConfigPartial,
|
||||
PodManifest, PodManifestConfig, PodMetaConfig, ProfileDiscovery, ProfileError, ProfileRegistry,
|
||||
ProfileRegistrySource, ProfileResolveOptions, ProfileResolver, ProfileSelector, ScopeConfig,
|
||||
ScopeRule, SessionConfigPartial, SharedScope, ToolOutputLimitsPartial, WorkerManifestConfig,
|
||||
CompactionConfigPartial, DelegationScope, FileUploadLimitsPartial, Permission,
|
||||
PermissionConfigPartial, PodManifest, PodManifestConfig, PodMetaConfig, ProfileDiscovery,
|
||||
ProfileError, ProfileRegistry, ProfileRegistrySource, ProfileResolveOptions, ProfileResolver,
|
||||
ProfileSelector, ScopeConfig, ScopeRule, SessionConfigPartial, SharedScope,
|
||||
ToolOutputLimitsPartial, WorkerManifestConfig,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tokio::net::UnixStream;
|
||||
@@ -54,7 +55,8 @@ struct SpawnPodInput {
|
||||
/// First message sent to the spawned Pod via `Method::Run`.
|
||||
task: String,
|
||||
/// Allow rules delegated to the spawned Pod. Must be a subset of the
|
||||
/// spawner's effective write scope.
|
||||
/// spawner's explicit delegation authority; direct tool scope alone is not
|
||||
/// sufficient.
|
||||
scope: Vec<ScopeRuleInput>,
|
||||
}
|
||||
|
||||
@@ -248,6 +250,10 @@ pub struct SpawnPodTool {
|
||||
/// `effective_write` semantics: Write is the only permission
|
||||
/// tracked across Pods, so revocation only touches Write.
|
||||
spawner_scope: SharedScope,
|
||||
/// Filesystem scope this Pod is allowed to subdelegate to children.
|
||||
/// This is intentionally separate from `spawner_scope`, which authorizes
|
||||
/// the current Pod's own direct tools.
|
||||
delegation_scope: DelegationScope,
|
||||
}
|
||||
|
||||
impl SpawnPodTool {
|
||||
@@ -261,6 +267,7 @@ impl SpawnPodTool {
|
||||
spawner_manifest: PodManifest,
|
||||
available_profiles: AvailableProfiles,
|
||||
spawner_scope: SharedScope,
|
||||
delegation_scope: DelegationScope,
|
||||
runtime_command: Option<PodRuntimeCommand>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -274,6 +281,7 @@ impl SpawnPodTool {
|
||||
spawner_manifest,
|
||||
available_profiles,
|
||||
spawner_scope,
|
||||
delegation_scope,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,6 +303,7 @@ impl Tool for SpawnPodTool {
|
||||
}
|
||||
|
||||
let scope_allow = parse_scope(&input.scope)?;
|
||||
self.validate_delegation_scope(&scope_allow)?;
|
||||
|
||||
let spawn_selector =
|
||||
parse_spawn_profile_selector(input.profile.as_deref()).map_err(|msg| {
|
||||
@@ -471,6 +480,28 @@ impl SpawnPodTool {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_delegation_scope(&self, scope_allow: &[ScopeRule]) -> Result<(), ToolError> {
|
||||
if self.delegation_scope.is_empty() && !scope_allow.is_empty() {
|
||||
return Err(ToolError::InvalidArgument(
|
||||
"SpawnPod requires delegation authority, but this Pod has no delegation scope grant; direct filesystem scope only authorizes this Pod's own tools".into(),
|
||||
));
|
||||
}
|
||||
for rule in scope_allow {
|
||||
let allowed = self
|
||||
.delegation_scope
|
||||
.allows_rule(rule)
|
||||
.map_err(|error| ToolError::InvalidArgument(error.to_string()))?;
|
||||
if !allowed {
|
||||
return Err(ToolError::InvalidArgument(format!(
|
||||
"requested child scope {} {:?} is outside this Pod's delegation scope grant",
|
||||
rule.target.display(),
|
||||
rule.permission
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn release_reservation(&self, lock_path: &Path, pod_name: &str) {
|
||||
if let Ok(mut g) = LockFileGuard::open(lock_path) {
|
||||
let _ = pod_registry::release_pod(&mut g, pod_name);
|
||||
@@ -654,6 +685,8 @@ fn manifest_to_reusable_config(manifest: &PodManifest) -> PodManifestConfig {
|
||||
allow: manifest.scope.allow.clone(),
|
||||
deny: manifest.scope.deny.clone(),
|
||||
},
|
||||
// `inherit` reuses behavioral configuration, not subdelegation authority.
|
||||
delegation_scope: ScopeConfig::default(),
|
||||
session: Some(SessionConfigPartial {
|
||||
record_event_trace: Some(manifest.session.record_event_trace),
|
||||
}),
|
||||
@@ -857,6 +890,8 @@ fn spawn_pod_tool_impl(
|
||||
spawner_manifest.clone(),
|
||||
available_profiles,
|
||||
spawner_scope.clone(),
|
||||
DelegationScope::from_config(&spawner_manifest.delegation_scope)
|
||||
.expect("resolved Pod manifest has a valid delegation scope"),
|
||||
runtime_command.clone(),
|
||||
));
|
||||
(meta, tool)
|
||||
|
||||
Reference in New Issue
Block a user