Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/crates/assembly/core/src/agentic/persistence/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -651,8 +651,8 @@ impl PersistenceManager {
/// Resolve the on-disk sessions directory for `workspace_path`.
///
/// Callers may pass either a logical workspace root or an already-resolved
/// managed sessions directory. Local workspace roots are slugified under
/// `~/.openbitfun/projects/`; already-resolved local/remote sessions
/// managed sessions directory. Local workspace roots use compact runtime
/// keys under `~/.openbitfun/projects/`; already-resolved local/remote sessions
/// directories are used as-is.
fn project_sessions_dir(&self, workspace_path: &Path) -> PathBuf {
if self.is_resolved_sessions_dir(workspace_path) {
Expand Down Expand Up @@ -8065,7 +8065,7 @@ mod tests {
assert!(runtime.snapshot_by_hash_dir.exists());
assert!(runtime.snapshot_metadata_dir.exists());
assert!(runtime.snapshot_operations_dir.exists());
assert!(runtime.plans_dir.exists());
assert!(!runtime.runtime_root.join("plans").exists());
assert!(runtime.layout_state_file.exists());
}

Expand Down
22 changes: 10 additions & 12 deletions src/crates/assembly/core/src/agentic/session/session_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10022,6 +10022,8 @@ mod tests {
SessionRelationship, SessionRelationshipKind, ToolCallData, ToolItemData, ToolResultData,
TurnStatus, UserMessageData,
};
#[cfg(feature = "remote-workspace")]
use crate::service::WorkspaceRuntimeService;
use crate::util::errors::OpenBitFunError;
use dashmap::{try_result::TryResult, DashMap};
use openbitfun_core_types::{
Expand Down Expand Up @@ -15019,12 +15021,13 @@ mod tests {
let workspace = TestWorkspace::new();
let path_manager = workspace.path_manager();
let port = CoreSessionStorePort::with_path_manager_for_tests(path_manager.clone());
let sessions_dir =
openbitfun_services_integrations::remote_ssh::remote_workspace_session_mirror_dir(
path_manager.remote_ssh_mirror_root_dir(),
"example-host",
"/root/repo",
);
WorkspaceRuntimeService::new(path_manager.clone())
.ensure_remote_workspace_runtime("example-host", "/root/repo")
.await
.expect("remote runtime should be ensured");
let runtime = WorkspaceRuntimeService::new(path_manager.clone())
.context_for_remote_workspace("example-host", "/root/repo");
let sessions_dir = runtime.sessions_dir;
let resolved = port
.resolve_session_storage_path(SessionStoragePathRequest {
workspace_path: sessions_dir.clone(),
Expand All @@ -15037,12 +15040,7 @@ mod tests {
assert_eq!(resolved.storage_kind, SessionStorageKind::Remote);
assert_eq!(resolved.effective_storage_path, sessions_dir);

let runtime_root =
openbitfun_services_integrations::remote_ssh::remote_workspace_runtime_root(
path_manager.remote_ssh_mirror_root_dir(),
"example-host",
"/root/repo",
);
let runtime_root = runtime.runtime_root;
let runtime_root_resolution = port
.resolve_session_storage_path(SessionStoragePathRequest {
workspace_path: runtime_root.clone(),
Expand Down
33 changes: 31 additions & 2 deletions src/crates/assembly/core/src/agentic/session/session_store_port.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,32 @@ impl CoreSessionStorePort {
.is_some_and(|candidate| candidate == projects_root)
}

fn project_runtime_sessions_kind(
path_manager: &PathManager,
path: &Path,
) -> SessionStorageKind {
let Some(runtime_root) = path.parent() else {
return SessionStorageKind::Local;
};
let state_path = runtime_root
.join("config")
.join("runtime_layout_state.json");
let Ok(bytes) = std::fs::read(state_path) else {
return SessionStorageKind::Local;
};
let Ok(state) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
return SessionStorageKind::Local;
};
if state.get("target_kind").and_then(serde_json::Value::as_str)
== Some("remote_workspace_mirror")
&& Self::is_confined_to_managed_root(&path_manager.projects_root(), path)
{
SessionStorageKind::Remote
} else {
SessionStorageKind::Local
}
}

pub(crate) fn resolved_sessions_dir_kind(
path_manager: &PathManager,
path: &Path,
Expand Down Expand Up @@ -185,8 +211,11 @@ impl CoreSessionStorePort {
.parent()
.and_then(|runtime_root| runtime_root.parent())
.is_some_and(|candidate| candidate == projects_root.as_path());
(has_local_shape && Self::is_confined_to_managed_root(&projects_root, path))
.then_some(SessionStorageKind::Local)
if has_local_shape && Self::is_confined_to_managed_root(&projects_root, path) {
Some(Self::project_runtime_sessions_kind(path_manager, path))
} else {
None
}
}
}

Expand Down
30 changes: 25 additions & 5 deletions src/crates/assembly/core/src/agentic/tools/file_permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,10 @@ fn is_current_workspace_plan_path(
return Ok(false);
}

let plans_root = context.current_workspace_runtime_root()?.join("plans");
let workspace_root = context.workspace_root().ok_or_else(|| {
OpenBitFunError::validation("A workspace is required for plan permissions".to_string())
})?;
let plans_root = get_path_manager_arc().project_plans_dir(workspace_root);
is_local_path_within_root(Path::new(&resolved.resolved_path), &plans_root)
}

Expand All @@ -157,8 +160,6 @@ fn openbitfun_managed_local_roots(context: &ToolUseContext) -> OpenBitFunResult<
return Ok(roots);
}

let runtime_root = context.current_workspace_runtime_root()?;
roots.push(runtime_root.join("plans"));
if let Some(session_id) = context.session_id.as_deref() {
roots.push(
context
Expand Down Expand Up @@ -317,7 +318,7 @@ mod tests {
let workspace = temp.path().join("workspace");
let runtime_root = temp.path().join("runtime");
let terminal_root = temp.path().join("terminals");
let plan = runtime_root.join("plans/plan.plan.md");
let plan = workspace.join(".openbitfun/plans/plan.plan.md");
let reference = runtime_root.join("sessions/session-1/artifacts/session-references/ref.md");
let compression =
runtime_root.join("sessions/session-1/artifacts/compression-transcripts/turn.md");
Expand Down Expand Up @@ -377,7 +378,7 @@ mod tests {
let temp = tempfile::tempdir().expect("temp dir");
let workspace = temp.path().join("workspace");
let runtime_root = temp.path().join("runtime");
let plan = runtime_root.join("plans/plan.plan.md");
let plan = workspace.join(".openbitfun/plans/plan.plan.md");
let transcript =
runtime_root.join("sessions/session-1/artifacts/compression-transcripts/turn.md");
fs::create_dir_all(&workspace).expect("workspace dir");
Expand Down Expand Up @@ -440,6 +441,25 @@ mod tests {
assert_eq!(transcript_edit[0].action, "edit");
}

#[test]
fn project_openbitfun_files_are_not_globally_exempt_from_edit_permission() {
let temp = tempfile::tempdir().expect("temp dir");
let workspace = temp.path().join("workspace");
let config_file = workspace.join(".openbitfun/config/settings.json");
fs::create_dir_all(config_file.parent().expect("config parent")).expect("config dir");
fs::write(&config_file, "{}").expect("config file");

let context =
ToolUseContext::for_tool_listing(Some(WorkspaceBinding::new(None, workspace)), None);
let file_path = config_file.to_string_lossy();
let intents = file_permission_intents("edit", [file_path.as_ref()], &context)
.expect("project config permission intent");

assert_eq!(intents.len(), 1);
assert_eq!(intents[0].action, "edit");
assert_eq!(intents[0].resources.len(), 1);
}

#[test]
fn unmanaged_runtime_paths_still_add_external_directory_intent() {
let temp = tempfile::tempdir().expect("temp dir");
Expand Down
12 changes: 4 additions & 8 deletions src/crates/assembly/core/src/agentic/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,7 @@ mod tests {
use openbitfun_core_types::{
SessionExecutionTarget, SessionExecutionTargetKind, WorktreeLifecycle,
};
use openbitfun_services_core::workspace_identity::{
remote_workspace_session_mirror_dir, workspace_session_identity,
};
use openbitfun_services_core::workspace_identity::workspace_session_identity;
use std::path::PathBuf;

#[test]
Expand All @@ -249,11 +247,9 @@ mod tests {
assert!(matches!(binding.backend, WorkspaceBackend::Remote { .. }));
assert_eq!(
binding.session_storage_dir(),
remote_workspace_session_mirror_dir(
crate::infrastructure::get_path_manager_arc().remote_ssh_mirror_root_dir(),
"127.0.0.1",
"/home/wsp/projects/test"
)
WorkspaceRuntimeService::new(crate::infrastructure::get_path_manager_arc())
.context_for_remote_workspace("127.0.0.1", "/home/wsp/projects/test")
.sessions_dir
);
}

Expand Down
Loading
Loading