Isolate sessions by user/repo/session scope
Pi data was shared across all workspaces since every mount appeared
as /workspace. Now auth is user-scoped, session history is per-repo
(keyed by workspace path hash), and each invocation gets its own
compose file and docker project name.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/src/main.rs b/src/main.rs
index c18afff..b620d45 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,5 @@
+use std::collections::hash_map::DefaultHasher;
+use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode, Stdio};
@@ -42,30 +44,41 @@ fn run() -> Result<()> {
.wrap_err_with(|| format!("workspace path does not exist: {}", cli.workspace.display()))?;
let xdg = xdg::BaseDirectories::with_prefix("ramekin");
- let pi_data_dir = xdg
- .create_data_directory("")
- .wrap_err("failed to create pi data directory")?;
- let pi_config_dir = xdg
+ // User-scoped: config files shared across all sessions
+ let config_dir = xdg
.create_config_directory("")
- .wrap_err("failed to create pi config directory")?;
+ .wrap_err("failed to create config directory")?;
- // Seed empty config files if they don't exist
for file in ["settings.json", "keybindings.json"] {
- let path = pi_config_dir.join(file);
- if !path.exists() {
+ let path = config_dir.join(file);
+ if !path.is_file() {
fs_err::write(&path, "{}")?;
}
}
- let agents_md = pi_config_dir.join("AGENTS.md");
+ let agents_md = config_dir.join("AGENTS.md");
if !agents_md.exists() {
fs_err::write(&agents_md, "")?;
}
- info!(data = %pi_data_dir.display(), config = %pi_config_dir.display(), "pi directories");
+ // User-scoped: auth shared across all sessions
+ let auth_file = xdg
+ .place_data_file("auth.json")
+ .wrap_err("failed to create auth file path")?;
+ if !auth_file.exists() {
+ fs_err::write(&auth_file, "{}")?;
+ }
+
+ // Repo-scoped: per-workspace pi data dir for sessions
+ let repo_slug = repo_slug(&workspace);
+ let repo_data_dir = xdg
+ .create_data_directory(format!("repos/{repo_slug}"))
+ .wrap_err("failed to create repo data directory")?;
+
+ info!(config = %config_dir.display(), repo = %repo_data_dir.display(), "directories");
info!(workspace = %workspace.display(), "starting agent");
- // Write embedded files to XDG cache so docker compose can find them
+ // User-scoped: embedded assets
let cache_dir = xdg
.create_cache_directory("")
.wrap_err("failed to create cache directory")?;
@@ -97,21 +110,30 @@ fn run() -> Result<()> {
(base_dockerfile, cache_dir.clone())
};
+ // Session-scoped: unique compose file and project name
+ let session_id = session_id();
+ let session_dir = xdg
+ .create_cache_directory(format!("sessions/{session_id}"))
+ .wrap_err("failed to create session directory")?;
+
let compose = generate_compose(
&workspace,
&dockerfile,
&build_context,
- &pi_data_dir,
- &pi_config_dir,
+ &repo_data_dir,
+ &auth_file,
+ &config_dir,
&extension_path,
);
- let compose_file = cache_dir.join("compose.yml");
+ let compose_file = session_dir.join("compose.yml");
fs_err::write(&compose_file, &compose)?;
+ let project_name = format!("ramekin-{session_id}");
let docker_compose = |args: &[&str]| -> Result<Command> {
let mut cmd = Command::new("docker");
cmd.args(["compose", "-f"])
.arg(&compose_file)
+ .args(["--project-name", &project_name])
.args(args);
Ok(cmd)
};
@@ -140,6 +162,11 @@ fn run() -> Result<()> {
error!("docker compose down failed ({})", down_status);
}
+ // Clean up session directory
+ if let Err(e) = fs_err::remove_dir_all(&session_dir) {
+ error!("failed to clean up session dir: {e}");
+ }
+
if !status.success() {
bail!("agent exited with error ({})", status);
}
@@ -147,6 +174,23 @@ fn run() -> Result<()> {
Ok(())
}
+/// Generate a short random session ID.
+fn session_id() -> String {
+ format!("{:x}", std::process::id())
+}
+
+/// Create a slug for a workspace path: `<dirname>-<hash>`.
+fn repo_slug(workspace: &Path) -> String {
+ let name = workspace
+ .file_name()
+ .map(|n| n.to_string_lossy().to_string())
+ .unwrap_or_else(|| "root".into());
+ let mut hasher = DefaultHasher::new();
+ workspace.hash(&mut hasher);
+ let hash = hasher.finish();
+ format!("{name}-{hash:08x}")
+}
+
#[derive(Serialize)]
struct ComposeConfig {
services: Services,
@@ -177,13 +221,15 @@ fn generate_compose(
workspace: &Path,
dockerfile: &Path,
build_context: &Path,
- data_dir: &Path,
+ repo_data_dir: &Path,
+ auth_file: &Path,
config_dir: &Path,
extension_path: &Path,
) -> String {
let mut volumes = vec![
format!("{}:/workspace", workspace.display()),
- format!("{}:/root/.pi", data_dir.display()),
+ format!("{}:/root/.pi", repo_data_dir.display()),
+ format!("{}:/root/.pi/auth.json", auth_file.display()),
format!(
"{}/settings.json:/root/.pi/agent/settings.json",
config_dir.display()