use super::*;
pub(super) fn validate_chat_workspace(
path: &Path,
state_dir: &Path,
tls: Option<&TlsConfig>,
) -> Result<PathBuf> {
let path = fs::canonicalize(path)?;
if !path.is_dir() || path.parent().is_none() {
return Err(Error::Config(
"workspace must be an existing non-root directory".into(),
));
}
validate_workspace_boundaries(&path, state_dir, tls)?;
Ok(path)
}
pub(crate) fn create_workspace_directory(
parent: &Path,
name: &str,
state_dir: &Path,
tls: Option<&TlsConfig>,
) -> Result<PathBuf> {
let parent = fs::canonicalize(parent)?;
if !parent.is_dir() {
return Err(Error::Config("workspace parent must be a directory".into()));
}
let name = name.trim();
if name.is_empty()
|| name.len() > MAX_WORKSPACE_DIRECTORY_NAME_BYTES
|| name.as_bytes().contains(&0)
|| name.bytes().any(|byte| byte == b'/' || byte == b'\\')
{
return Err(Error::Config(format!(
"workspace directory name must be 1–{MAX_WORKSPACE_DIRECTORY_NAME_BYTES} bytes and contain no path separators"
)));
}
let mut components = Path::new(name).components();
if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() {
return Err(Error::Config(
"workspace directory name must be one path component".into(),
));
}
let path = parent.join(name);
validate_workspace_boundaries(&path, state_dir, tls)?;
match fs::symlink_metadata(&path) {
Ok(_) => {
return Err(Error::Config("workspace directory already exists".into()));
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
fs::create_dir(&path)?;
let created = validate_chat_workspace(&path, state_dir, tls).and_then(|path| {
initialize_workspace_repository(&path)?;
Ok(path)
});
match created {
Ok(path) => Ok(path),
Err(error) => {
let _ = fs::remove_dir_all(&path);
Err(error)
}
}
}
fn initialize_workspace_repository(path: &Path) -> Result<()> {
let mut command = std::process::Command::new("git");
command
.args(["init", "--quiet", "--initial-branch", "main"])
.env("GIT_TERMINAL_PROMPT", "0")
.env("LC_ALL", "C")
.current_dir(path);
for name in crate::sandbox::REPOSITORY_LOCAL_GIT_ENVIRONMENT {
command.env_remove(name);
}
let output = command.output()?;
if !output.status.success() {
return Err(Error::Config(
"failed to initialize workspace Git repository".into(),
));
}
Ok(())
}
fn validate_workspace_boundaries(
path: &Path,
state_dir: &Path,
tls: Option<&TlsConfig>,
) -> Result<()> {
let state_dir = fs::canonicalize(state_dir)?;
if path.starts_with(&state_dir) || state_dir.starts_with(path) {
return Err(Error::Config(
"gateway state directory and chat workspace must not overlap".into(),
));
}
let extensions = crate::extensions::extensions_path(&state_dir);
if path.starts_with(&extensions)
|| extensions.starts_with(path)
|| fs::canonicalize(&extensions)
.is_ok_and(|extensions| path.starts_with(&extensions) || extensions.starts_with(path))
{
return Err(Error::Config(
"extension store and chat workspace must not overlap".into(),
));
}
if tls.is_some_and(|tls| {
fs::canonicalize(&tls.private_key).is_ok_and(|key| key.starts_with(path))
}) {
return Err(Error::Config(
"TLS private key must be stored outside every chat workspace".into(),
));
}
Ok(())
}
pub(super) fn workspace_id(path: &Path) -> String {
let digest = sha2::Sha256::digest(path.as_os_str().as_encoded_bytes());
let mut id = String::from("path-v1:");
for byte in digest {
use std::fmt::Write as _;
write!(&mut id, "{byte:02x}").expect("writing to a string cannot fail");
}
id
}
pub(crate) fn local_user_name() -> Option<String> {
["USER", "USERNAME"]
.into_iter()
.find_map(|name| env::var(name).ok().filter(|value| !value.trim().is_empty()))
}