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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ cross-platform icons, CI, Dependabot, security policy, and contribution guide.
Version tags publish Windows installers through the
[`Windows Release`](.github/workflows/windows-release.yml) GitHub Actions
workflow. The tag must match the version in `src-tauri/tauri.conf.json`; for
example, version `0.1.2` is released with tag `v0.1.2`.
example, version `0.1.3` is released with tag `v0.1.3`.

The tagged GitHub prerelease contains:

Expand Down
2 changes: 1 addition & 1 deletion crates/open-profiler-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "opensoft-open-profiler-core"
version = "0.1.2"
version = "0.1.3"
description = "Secure LLM profile discovery and activation for openProfiler"
edition.workspace = true
license.workspace = true
Expand Down
176 changes: 119 additions & 57 deletions crates/open-profiler-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const LEGACY_ACTIVE_MARKER: &str = ".profile-switcher-active.json";
const DESKTOP_ROLLBACK_CREDENTIAL: &str = ".openprofiler-desktop-auth.rollback.json";
const DESKTOP_ROLLBACK_MARKER: &str = ".openprofiler-desktop-rollback.json";
const MAX_CREDENTIAL_BYTES: u64 = 1024 * 1024;
const MAX_PROFILE_DIRECTORY_DEPTH: usize = 7;

#[derive(Debug, Error)]
pub enum ProfileError {
Expand Down Expand Up @@ -608,6 +609,15 @@ pub fn activate_profile(
}

pub fn codex_desktop_status(config: &DiscoveryConfig, desktop_home: &Path) -> CodexDesktopStatus {
let inventory = discover_profiles(config);
codex_desktop_status_from_inventory(config, desktop_home, &inventory)
}

pub fn codex_desktop_status_from_inventory(
config: &DiscoveryConfig,
desktop_home: &Path,
inventory: &ProfileInventory,
) -> CodexDesktopStatus {
let Some(provider_config) = provider_config(config, Provider::Codex) else {
return CodexDesktopStatus {
file_activation_supported: false,
Expand All @@ -620,7 +630,7 @@ pub fn codex_desktop_status(config: &DiscoveryConfig, desktop_home: &Path) -> Co
};

let credential_state = codex_desktop_credential_state(desktop_home);
let profiles = codex_profile_credentials(config, provider_config);
let profiles = codex_profile_credentials_from_inventory(inventory, provider_config);
let mut eligible_profile_paths = profiles
.iter()
.map(|profile| profile.profile_path.clone())
Expand Down Expand Up @@ -866,19 +876,27 @@ fn provider_config(config: &DiscoveryConfig, provider: Provider) -> Option<&Prov
fn codex_profile_credentials(
config: &DiscoveryConfig,
provider_config: &ProviderConfig,
) -> Vec<CodexProfileCredential> {
let inventory = discover_profiles(config);
codex_profile_credentials_from_inventory(&inventory, provider_config)
}

fn codex_profile_credentials_from_inventory(
inventory: &ProfileInventory,
provider_config: &ProviderConfig,
) -> Vec<CodexProfileCredential> {
let profile_root = provider_config.profiles_home.join("profiles");
discover_profiles(config)
inventory
.profiles
.into_iter()
.iter()
.filter(|profile| profile.provider == Provider::Codex && profile.credential_present)
.filter_map(|profile| {
let profile_dir = checked_profile_dir(&profile_root, &profile.profile_path).ok()?;
let credential_path = profile_dir.join(Provider::Codex.credential_name());
let identity = read_codex_credential_identity(&credential_path).ok()?;
Some(CodexProfileCredential {
name: profile.name,
profile_path: profile.profile_path,
name: profile.name.clone(),
profile_path: profile.profile_path.clone(),
credential_path,
identity,
})
Expand Down Expand Up @@ -1052,66 +1070,66 @@ fn discover_provider(config: &ProviderConfig) -> (Vec<Profile>, Vec<String>) {

let profile_root = config.profiles_home.join("profiles");
if profile_root.is_dir() {
for entry in WalkDir::new(&profile_root)
.min_depth(2)
.max_depth(8)
let mut entries = WalkDir::new(&profile_root)
.min_depth(1)
.max_depth(MAX_PROFILE_DIRECTORY_DEPTH)
.follow_links(false)
.into_iter()
.filter_map(std::result::Result::ok)
.filter(|entry| entry.file_type().is_file() && entry.file_name() == ".profile.json")
{
let metadata_path = entry.into_path();
let result = read_manifest_profile(&metadata_path).and_then(|mut profile| {
if profile.profile_path.is_none() {
let parent =
metadata_path
.parent()
.ok_or_else(|| ProfileError::InvalidProfile {
profile: profile.name.clone(),
reason: "metadata file has no parent directory".to_string(),
})?;
let relative = parent
.strip_prefix(&profile_root)
.map_err(|_| ProfileError::EscapedProfileRoot(parent.to_path_buf()))?;
profile.profile_path = Some(relative.to_string_lossy().into_owned());
}
resolve_profile(
profile,
config,
ProfileSource::ProfileMetadata,
&active_marker,
)
});
.into_iter();

while let Some(entry) = entries.next() {
let Ok(entry) = entry else {
continue;
};
if !entry.file_type().is_dir() {
continue;
}

match result {
Ok(profile) => {
profiles
.entry(normalized_path_key(&profile.profile_path))
.or_insert(profile);
let profile_dir = entry.into_path();
let metadata_path = profile_dir.join(".profile.json");
let credential_path = profile_dir.join(config.provider.credential_name());
let has_metadata = ensure_regular_nonempty_file(&metadata_path).is_some();
let has_credential = ensure_regular_nonempty_file(&credential_path).is_some();
if !has_metadata && !has_credential {
continue;
}

// A profile owns everything beneath its directory. Avoid walking
// provider caches, histories, logs, and databases over WSL UNC.
entries.skip_current_dir();

if has_metadata {
let result = read_manifest_profile(&metadata_path).and_then(|mut profile| {
if profile.profile_path.is_none() {
let relative = profile_dir
.strip_prefix(&profile_root)
.map_err(|_| ProfileError::EscapedProfileRoot(profile_dir.clone()))?;
profile.profile_path = Some(relative_profile_path(relative));
}
resolve_profile(
profile,
config,
ProfileSource::ProfileMetadata,
&active_marker,
)
});

match result {
Ok(profile) => {
profiles
.entry(normalized_path_key(&profile.profile_path))
.or_insert(profile);
}
Err(error) => issues.push(error.to_string()),
}
Err(error) => issues.push(error.to_string()),
}
}

for entry in WalkDir::new(&profile_root)
.min_depth(2)
.max_depth(8)
.follow_links(false)
.into_iter()
.filter_map(std::result::Result::ok)
.filter(|entry| {
entry.file_type().is_file()
&& entry.file_name() == config.provider.credential_name()
})
{
let credential_path = entry.into_path();
let Some(parent) = credential_path.parent() else {
if !has_credential {
continue;
};
let Ok(relative) = parent.strip_prefix(&profile_root) else {
}
let Ok(relative) = profile_dir.strip_prefix(&profile_root) else {
continue;
};
let profile_path = relative.to_string_lossy().into_owned();
let profile_path = relative_profile_path(relative);
let key = normalized_path_key(&profile_path);
if profiles.contains_key(&key) {
continue;
Expand Down Expand Up @@ -1565,6 +1583,10 @@ fn normalized_path_key(path: &str) -> String {
path.replace('\\', "/").to_lowercase()
}

fn relative_profile_path(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}

fn read_text(path: &Path) -> Result<String> {
let mut file = open_file_no_follow(path)?;
let mut text = String::new();
Expand Down Expand Up @@ -1738,6 +1760,46 @@ mod tests {
assert_eq!(profile.source, ProfileSource::ProfileDirectory);
}

#[test]
fn stops_walking_when_a_profile_directory_is_found() {
let temp = TempDir::new().unwrap();
let config = config(&temp);
let codex = provider_config(&config, Provider::Codex);
let profile_dir = codex.profiles_home.join("profiles/client/account-one");
write_file(
&profile_dir.join(".profile.json"),
r#"{"name":"account-one","family":"client"}"#,
);
write_file(&profile_dir.join("auth.json"), "codex-secret");
write_file(
&profile_dir.join("cache/nested/.profile.json"),
r#"{"name":"should-not-be-scanned","family":"cache"}"#,
);
write_file(&profile_dir.join("cache/nested/auth.json"), "cached-secret");

let inventory = discover_profiles(&config);
assert_eq!(inventory.profiles.len(), 1);
assert_eq!(inventory.profiles[0].name, "account-one");
assert_eq!(inventory.profiles[0].profile_path, "client/account-one");
}

#[test]
fn empty_placeholders_do_not_hide_nested_profiles() {
let temp = TempDir::new().unwrap();
let config = config(&temp);
let codex = provider_config(&config, Provider::Codex);
let family_dir = codex.profiles_home.join("profiles/client");
write_file(&family_dir.join(".profile.json"), "");
write_file(&family_dir.join("auth.json"), "");
write_file(&family_dir.join("account-one/auth.json"), "codex-secret");

let inventory = discover_profiles(&config);
assert_eq!(inventory.profiles.len(), 1);
assert_eq!(inventory.profiles[0].name, "account-one");
assert_eq!(inventory.profiles[0].profile_path, "client/account-one");
assert!(inventory.profiles[0].credential_present);
}

#[test]
fn malformed_provider_manifest_does_not_hide_other_provider() {
let temp = TempDir::new().unwrap();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@opensoft/open-profiler",
"private": true,
"version": "0.1.2",
"version": "0.1.3",
"description": "A full LLM profile manager",
"license": "Apache-2.0",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "opensoft-open-profiler"
version = "0.1.2"
version = "0.1.3"
description = "A full LLM profile manager"
edition.workspace = true
license.workspace = true
Expand Down
Loading
Loading