fix: scope runtime workdir attachments independently
This commit is contained in:
@@ -426,6 +426,7 @@ struct LocalWorkdirSessionInner {
|
|||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
pinned_root: Option<Arc<std::fs::File>>,
|
pinned_root: Option<Arc<std::fs::File>>,
|
||||||
scope: SharedScope,
|
scope: SharedScope,
|
||||||
|
command_output_scope: SharedScope,
|
||||||
cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
capabilities: WorkdirSessionCapabilities,
|
capabilities: WorkdirSessionCapabilities,
|
||||||
read_limits: Option<BoundedReadLimits>,
|
read_limits: Option<BoundedReadLimits>,
|
||||||
@@ -643,12 +644,37 @@ impl LocalWorkdirSession {
|
|||||||
capabilities: WorkdirSessionCapabilities,
|
capabilities: WorkdirSessionCapabilities,
|
||||||
command_environment: BTreeMap<String, String>,
|
command_environment: BTreeMap<String, String>,
|
||||||
resources: Vec<Arc<dyn WorkdirSessionResource>>,
|
resources: Vec<Arc<dyn WorkdirSessionResource>>,
|
||||||
|
) -> Self {
|
||||||
|
Self::materialized_bound_with_environment_and_command_output_scope(
|
||||||
|
workdir,
|
||||||
|
root,
|
||||||
|
cwd,
|
||||||
|
scope.clone(),
|
||||||
|
scope,
|
||||||
|
capabilities,
|
||||||
|
command_environment,
|
||||||
|
resources,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a local provider session whose Workdir filesystem authority is
|
||||||
|
/// independent from the provider-owned command output authority.
|
||||||
|
pub fn materialized_bound_with_environment_and_command_output_scope(
|
||||||
|
workdir: Workdir,
|
||||||
|
root: PathBuf,
|
||||||
|
cwd: PathBuf,
|
||||||
|
scope: SharedScope,
|
||||||
|
command_output_scope: SharedScope,
|
||||||
|
capabilities: WorkdirSessionCapabilities,
|
||||||
|
command_environment: BTreeMap<String, String>,
|
||||||
|
resources: Vec<Arc<dyn WorkdirSessionResource>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::materialized_bound_with_policy(
|
Self::materialized_bound_with_policy(
|
||||||
workdir,
|
workdir,
|
||||||
root,
|
root,
|
||||||
cwd,
|
cwd,
|
||||||
scope,
|
scope,
|
||||||
|
command_output_scope,
|
||||||
capabilities,
|
capabilities,
|
||||||
command_environment,
|
command_environment,
|
||||||
resources,
|
resources,
|
||||||
@@ -698,11 +724,13 @@ impl LocalWorkdirSession {
|
|||||||
"External Workdir read scope could not be established".to_string(),
|
"External Workdir read scope could not be established".to_string(),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
let scope = SharedScope::new(scope);
|
||||||
Ok(Self::materialized_bound_with_policy(
|
Ok(Self::materialized_bound_with_policy(
|
||||||
workdir,
|
workdir,
|
||||||
root.clone(),
|
root.clone(),
|
||||||
root,
|
root,
|
||||||
SharedScope::new(scope),
|
scope.clone(),
|
||||||
|
scope,
|
||||||
WorkdirSessionCapabilities::READ_ONLY,
|
WorkdirSessionCapabilities::READ_ONLY,
|
||||||
BTreeMap::new(),
|
BTreeMap::new(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
@@ -729,6 +757,7 @@ impl LocalWorkdirSession {
|
|||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
scope: SharedScope,
|
scope: SharedScope,
|
||||||
|
command_output_scope: SharedScope,
|
||||||
capabilities: WorkdirSessionCapabilities,
|
capabilities: WorkdirSessionCapabilities,
|
||||||
command_environment: BTreeMap<String, String>,
|
command_environment: BTreeMap<String, String>,
|
||||||
resources: Vec<Arc<dyn WorkdirSessionResource>>,
|
resources: Vec<Arc<dyn WorkdirSessionResource>>,
|
||||||
@@ -742,6 +771,7 @@ impl LocalWorkdirSession {
|
|||||||
root,
|
root,
|
||||||
pinned_root,
|
pinned_root,
|
||||||
scope,
|
scope,
|
||||||
|
command_output_scope,
|
||||||
cwd,
|
cwd,
|
||||||
capabilities,
|
capabilities,
|
||||||
read_limits,
|
read_limits,
|
||||||
@@ -1154,7 +1184,11 @@ impl WorkdirSession for LocalWorkdirSession {
|
|||||||
self.ensure_capability(WorkdirSessionCapability::Command)?;
|
self.ensure_capability(WorkdirSessionCapability::Command)?;
|
||||||
self.ensure_open()?;
|
self.ensure_open()?;
|
||||||
if let Some(spill_dir) = request.spill_dir.as_deref()
|
if let Some(spill_dir) = request.spill_dir.as_deref()
|
||||||
&& !self.inner.scope.snapshot().is_readable(spill_dir)
|
&& !self
|
||||||
|
.inner
|
||||||
|
.command_output_scope
|
||||||
|
.snapshot()
|
||||||
|
.is_readable(spill_dir)
|
||||||
{
|
{
|
||||||
return Err(WorkdirError::OutOfScope(spill_dir.to_path_buf()));
|
return Err(WorkdirError::OutOfScope(spill_dir.to_path_buf()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -768,33 +768,73 @@ fn initial_workdir_session_capabilities(
|
|||||||
.unwrap_or(WorkdirSessionCapabilities::ALL)
|
.unwrap_or(WorkdirSessionCapabilities::ALL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn runtime_attachment_scope(
|
||||||
|
root: &Path,
|
||||||
|
capabilities: WorkdirSessionCapabilities,
|
||||||
|
) -> Result<manifest::SharedScope, String> {
|
||||||
|
if capabilities == WorkdirSessionCapabilities::EMPTY {
|
||||||
|
return Ok(manifest::SharedScope::new(manifest::Scope::empty()));
|
||||||
|
}
|
||||||
|
let writable = [
|
||||||
|
workdir::WorkdirSessionCapability::Write,
|
||||||
|
workdir::WorkdirSessionCapability::Edit,
|
||||||
|
workdir::WorkdirSessionCapability::Command,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.any(|capability| capabilities.supports(capability));
|
||||||
|
let scope = manifest::Scope::from_config(&manifest::ScopeConfig {
|
||||||
|
allow: vec![manifest::ScopeRule {
|
||||||
|
target: root.to_path_buf(),
|
||||||
|
permission: if writable {
|
||||||
|
manifest::Permission::Write
|
||||||
|
} else {
|
||||||
|
manifest::Permission::Read
|
||||||
|
},
|
||||||
|
recursive: true,
|
||||||
|
symlink_policy: manifest::SymlinkPolicy::Resolved,
|
||||||
|
}],
|
||||||
|
deny: Vec::new(),
|
||||||
|
})
|
||||||
|
.map_err(|error| format!("create Runtime-local attachment scope: {error}"))?;
|
||||||
|
Ok(manifest::SharedScope::new(scope))
|
||||||
|
}
|
||||||
|
|
||||||
fn runtime_local_workdir_session(
|
fn runtime_local_workdir_session(
|
||||||
workdir_id: &str,
|
workdir_id: &str,
|
||||||
root: &Path,
|
root: &Path,
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
scope: manifest::SharedScope,
|
scope: manifest::SharedScope,
|
||||||
|
command_output_scope: manifest::SharedScope,
|
||||||
capabilities: WorkdirSessionCapabilities,
|
capabilities: WorkdirSessionCapabilities,
|
||||||
command_environment: std::collections::BTreeMap<String, String>,
|
command_environment: std::collections::BTreeMap<String, String>,
|
||||||
resources: Vec<Arc<dyn workdir::WorkdirSessionResource>>,
|
resources: Vec<Arc<dyn workdir::WorkdirSessionResource>>,
|
||||||
) -> WorkdirSessionHandle {
|
) -> WorkdirSessionHandle {
|
||||||
Arc::new(LocalWorkdirSession::materialized_bound_with_environment(
|
Arc::new(
|
||||||
Workdir::new(workdir_id),
|
LocalWorkdirSession::materialized_bound_with_environment_and_command_output_scope(
|
||||||
root.to_path_buf(),
|
Workdir::new(workdir_id),
|
||||||
cwd.to_path_buf(),
|
root.to_path_buf(),
|
||||||
scope,
|
cwd.to_path_buf(),
|
||||||
capabilities,
|
scope,
|
||||||
command_environment,
|
command_output_scope,
|
||||||
resources,
|
capabilities,
|
||||||
))
|
command_environment,
|
||||||
|
resources,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runtime_local_workdir_router(
|
fn runtime_local_workdir_router(
|
||||||
attachments: &BTreeMap<WorkdirAttachmentAlias, WorkingDirectoryBinding>,
|
attachments: &BTreeMap<WorkdirAttachmentAlias, WorkingDirectoryBinding>,
|
||||||
capabilities: &BTreeMap<WorkdirAttachmentAlias, WorkdirSessionCapabilities>,
|
capabilities: &BTreeMap<WorkdirAttachmentAlias, WorkdirSessionCapabilities>,
|
||||||
scope: manifest::SharedScope,
|
command_output_scope: manifest::SharedScope,
|
||||||
) -> Result<Arc<WorkdirSessionRouter>, String> {
|
) -> Result<Arc<WorkdirSessionRouter>, String> {
|
||||||
let router = Arc::new(WorkdirSessionRouter::new());
|
let router = Arc::new(WorkdirSessionRouter::new());
|
||||||
for (alias, binding) in attachments {
|
for (alias, binding) in attachments {
|
||||||
|
let capabilities = capabilities
|
||||||
|
.get(alias)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(WorkdirSessionCapabilities::READ_ONLY);
|
||||||
|
let scope = runtime_attachment_scope(binding.root(), capabilities)?;
|
||||||
router
|
router
|
||||||
.attach(
|
.attach(
|
||||||
alias.clone(),
|
alias.clone(),
|
||||||
@@ -802,11 +842,9 @@ fn runtime_local_workdir_router(
|
|||||||
&binding.working_directory.id,
|
&binding.working_directory.id,
|
||||||
binding.root(),
|
binding.root(),
|
||||||
binding.cwd(),
|
binding.cwd(),
|
||||||
scope.clone(),
|
scope,
|
||||||
capabilities
|
command_output_scope.clone(),
|
||||||
.get(alias)
|
capabilities,
|
||||||
.copied()
|
|
||||||
.unwrap_or(WorkdirSessionCapabilities::READ_ONLY),
|
|
||||||
binding.command_environment(),
|
binding.command_environment(),
|
||||||
binding.session_resources(),
|
binding.session_resources(),
|
||||||
),
|
),
|
||||||
@@ -2221,11 +2259,13 @@ where
|
|||||||
format!("failed to create Workdir session scope: {error}"),
|
format!("failed to create Workdir session scope: {error}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
let scope = manifest::SharedScope::new(scope);
|
||||||
Ok(runtime_local_workdir_session(
|
Ok(runtime_local_workdir_session(
|
||||||
working_directory_id,
|
working_directory_id,
|
||||||
binding.root(),
|
binding.root(),
|
||||||
binding.cwd(),
|
binding.cwd(),
|
||||||
manifest::SharedScope::new(scope),
|
scope.clone(),
|
||||||
|
scope,
|
||||||
WorkdirSessionCapabilities::ALL,
|
WorkdirSessionCapabilities::ALL,
|
||||||
binding.command_environment(),
|
binding.command_environment(),
|
||||||
binding.session_resources(),
|
binding.session_resources(),
|
||||||
@@ -3044,7 +3084,28 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn restored_router_preserves_read_only_capabilities_for_local_attachment() {
|
fn runtime_attachment_scope_matches_session_capabilities() {
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let writable = runtime_attachment_scope(root.path(), WorkdirSessionCapabilities::ALL)
|
||||||
|
.unwrap()
|
||||||
|
.snapshot();
|
||||||
|
assert_eq!(
|
||||||
|
writable.permission_at(root.path()),
|
||||||
|
Some(manifest::Permission::Write)
|
||||||
|
);
|
||||||
|
|
||||||
|
let read_only =
|
||||||
|
runtime_attachment_scope(root.path(), WorkdirSessionCapabilities::READ_ONLY)
|
||||||
|
.unwrap()
|
||||||
|
.snapshot();
|
||||||
|
assert_eq!(
|
||||||
|
read_only.permission_at(root.path()),
|
||||||
|
Some(manifest::Permission::Read)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn restored_router_preserves_read_only_capabilities_for_local_attachment() {
|
||||||
let runtime_base = tempfile::tempdir().unwrap();
|
let runtime_base = tempfile::tempdir().unwrap();
|
||||||
let repo = create_clean_repo();
|
let repo = create_clean_repo();
|
||||||
let materializer = RuntimeGitMaterializer::new(runtime_base.path());
|
let materializer = RuntimeGitMaterializer::new(runtime_base.path());
|
||||||
@@ -3054,7 +3115,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let alias = WorkdirAttachmentAlias::new("docs").unwrap();
|
let alias = WorkdirAttachmentAlias::new("docs").unwrap();
|
||||||
let workdir_id = binding.working_directory.id.clone();
|
let workdir_id = binding.working_directory.id.clone();
|
||||||
let scope = manifest::SharedScope::new(manifest::Scope::writable(binding.root()).unwrap());
|
let command_output_scope = manifest::SharedScope::new(manifest::Scope::empty());
|
||||||
let attachments = BTreeMap::from([(alias.clone(), binding)]);
|
let attachments = BTreeMap::from([(alias.clone(), binding)]);
|
||||||
let logical = [LogicalWorkdirAttachment {
|
let logical = [LogicalWorkdirAttachment {
|
||||||
alias: alias.clone(),
|
alias: alias.clone(),
|
||||||
@@ -3066,12 +3127,38 @@ mod tests {
|
|||||||
))
|
))
|
||||||
.client_handle();
|
.client_handle();
|
||||||
|
|
||||||
let router = restored_workdir_router(&attachments, &logical, &[], scope, client).unwrap();
|
let router =
|
||||||
|
restored_workdir_router(&attachments, &logical, &[], command_output_scope, client)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let session = router.session(&alias).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
router.session(&alias).unwrap().capabilities(),
|
session.capabilities(),
|
||||||
WorkdirSessionCapabilities::READ_ONLY
|
WorkdirSessionCapabilities::READ_ONLY
|
||||||
);
|
);
|
||||||
|
let read = session
|
||||||
|
.read(workdir::ReadRequest {
|
||||||
|
path: workdir::WorkdirPath::new("README.md").unwrap(),
|
||||||
|
offset: 0,
|
||||||
|
limit: 10,
|
||||||
|
max_bytes: 1024,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(read.bytes, b"clean\n");
|
||||||
|
session
|
||||||
|
.authorize_scope_path(workdir::WorkdirScopeAuthorizationRequest {
|
||||||
|
rules: vec![workdir::WorkdirToolScopeRule {
|
||||||
|
target: workdir::WorkdirPath::root(),
|
||||||
|
permission: workdir::WorkdirToolScopePermission::Read,
|
||||||
|
recursive: true,
|
||||||
|
symlink_policy: manifest::SymlinkPolicy::Resolved,
|
||||||
|
}],
|
||||||
|
path: workdir::WorkdirPath::new("README.md").unwrap(),
|
||||||
|
permission: workdir::WorkdirToolScopePermission::Read,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4054,20 +4141,24 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn restore_opens_a_fresh_session_with_the_same_read_only_capabilities() {
|
fn restore_opens_a_fresh_session_with_the_same_read_only_capabilities() {
|
||||||
let root = tempfile::tempdir().unwrap();
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let spawned_scope = manifest::SharedScope::new(Scope::writable(root.path()).unwrap());
|
||||||
let spawned = runtime_local_workdir_session(
|
let spawned = runtime_local_workdir_session(
|
||||||
"working-directory-42",
|
"working-directory-42",
|
||||||
root.path(),
|
root.path(),
|
||||||
root.path(),
|
root.path(),
|
||||||
manifest::SharedScope::new(Scope::writable(root.path()).unwrap()),
|
spawned_scope.clone(),
|
||||||
|
spawned_scope,
|
||||||
WorkdirSessionCapabilities::READ_ONLY,
|
WorkdirSessionCapabilities::READ_ONLY,
|
||||||
Default::default(),
|
Default::default(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
);
|
);
|
||||||
|
let restored_scope = manifest::SharedScope::new(Scope::writable(root.path()).unwrap());
|
||||||
let restored = runtime_local_workdir_session(
|
let restored = runtime_local_workdir_session(
|
||||||
"working-directory-42",
|
"working-directory-42",
|
||||||
root.path(),
|
root.path(),
|
||||||
root.path(),
|
root.path(),
|
||||||
manifest::SharedScope::new(Scope::writable(root.path()).unwrap()),
|
restored_scope.clone(),
|
||||||
|
restored_scope,
|
||||||
WorkdirSessionCapabilities::READ_ONLY,
|
WorkdirSessionCapabilities::READ_ONLY,
|
||||||
Default::default(),
|
Default::default(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
|
|||||||
Reference in New Issue
Block a user