Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f2594712f
|
||
|
|
79d43c8791
|
||
|
|
5a5da90734
|
||
|
|
f2a0e7453e
|
||
|
|
0fe430102a
|
||
|
|
d13bd32fdf
|
Generated
+44
@@ -1016,6 +1016,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.0"
|
||||
@@ -1457,6 +1463,7 @@ dependencies = [
|
||||
"path-absolutize",
|
||||
"petgraph 0.7.1",
|
||||
"pretty_assertions",
|
||||
"qrcode",
|
||||
"rand 0.10.1",
|
||||
"reedline",
|
||||
"reqwest 0.13.4",
|
||||
@@ -3046,6 +3053,18 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@@ -3576,6 +3595,16 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb517913cfcfb9eeda59f36020269075a152701a01606c612f547e4890be399"
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
@@ -4418,6 +4447,21 @@ dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
|
||||
dependencies = [
|
||||
"image",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.4"
|
||||
|
||||
@@ -107,6 +107,7 @@ self_update = { version = "0.44", default-features = false, features = [
|
||||
"archive-zip",
|
||||
"compression-zip-deflate",
|
||||
] }
|
||||
qrcode = "0.14"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.13.3"
|
||||
|
||||
@@ -369,6 +369,18 @@ clients:
|
||||
# scopes: [openai.chat]
|
||||
# flow: client_credentials
|
||||
|
||||
# Example: OAuth via Device Authorization Grant (RFC 8628 — for CLIs like Moonshot's kimi-code, MiniMax mmx, etc.)
|
||||
# - type: openai-compatible
|
||||
# name: moonshot
|
||||
# api_base: https://api.kimi.com/coding/v1
|
||||
# auth: oauth
|
||||
# oauth:
|
||||
# client_id: '{{MOONSHOT_CLIENT_ID}}'
|
||||
# device_authorization_url: https://auth.kimi.com/api/oauth/device_authorization
|
||||
# token_url: https://auth.kimi.com/api/oauth/token
|
||||
# flow: device_code
|
||||
# # use_pkce_in_device_flow: true # enable if your provider requires PKCE with device flow
|
||||
|
||||
# See https://docs.ai21.com/docs/overview
|
||||
- type: openai-compatible
|
||||
name: ai12
|
||||
|
||||
+316
-1
@@ -32,6 +32,7 @@ pub enum OAuthFlow {
|
||||
#[default]
|
||||
Pkce,
|
||||
ClientCredentials,
|
||||
DeviceCode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -45,6 +46,7 @@ pub struct OAuthConfig {
|
||||
pub authorize_url: Option<String>,
|
||||
pub redirect_uri: Option<String>,
|
||||
pub redirect_port: Option<u16>,
|
||||
pub device_authorization_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub scopes: Vec<String>,
|
||||
pub token_request_format: Option<TokenRequestFormat>,
|
||||
@@ -56,6 +58,8 @@ pub struct OAuthConfig {
|
||||
pub extra_request_headers: IndexMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub echo_pkce_in_token_exchange: bool,
|
||||
#[serde(default)]
|
||||
pub use_pkce_in_device_flow: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub include_state_in_token_exchange: bool,
|
||||
}
|
||||
@@ -81,6 +85,9 @@ impl OAuthConfig {
|
||||
if override_cfg.redirect_port.is_some() {
|
||||
self.redirect_port = override_cfg.redirect_port;
|
||||
}
|
||||
if override_cfg.device_authorization_url.is_some() {
|
||||
self.device_authorization_url = override_cfg.device_authorization_url;
|
||||
}
|
||||
if !override_cfg.scopes.is_empty() {
|
||||
self.scopes = override_cfg.scopes;
|
||||
}
|
||||
@@ -101,6 +108,7 @@ impl OAuthConfig {
|
||||
}
|
||||
|
||||
self.echo_pkce_in_token_exchange = override_cfg.echo_pkce_in_token_exchange;
|
||||
self.use_pkce_in_device_flow = override_cfg.use_pkce_in_device_flow;
|
||||
self.include_state_in_token_exchange = override_cfg.include_state_in_token_exchange;
|
||||
|
||||
self
|
||||
@@ -158,6 +166,14 @@ pub trait OAuthProvider: Send + Sync {
|
||||
fn echo_pkce_in_token_exchange(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn device_authorization_url(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn use_pkce_in_device_flow(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -174,6 +190,7 @@ pub async fn run_oauth_flow(provider: &dyn OAuthProvider, client_name: &str) ->
|
||||
match provider.flow() {
|
||||
OAuthFlow::Pkce => run_pkce_flow(provider, client_name).await,
|
||||
OAuthFlow::ClientCredentials => run_client_credentials_flow(provider, client_name).await,
|
||||
OAuthFlow::DeviceCode => run_device_code_flow(provider, client_name).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +357,169 @@ async fn run_client_credentials_flow(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_device_code_flow(provider: &dyn OAuthProvider, client_name: &str) -> Result<()> {
|
||||
let device_auth_url = provider.device_authorization_url().ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Provider '{}' is configured with flow: device_code but has no device_authorization_url. \
|
||||
Set `oauth.device_authorization_url` in your config.",
|
||||
provider.provider_name()
|
||||
)
|
||||
})?;
|
||||
|
||||
let client = ReqwestClient::new();
|
||||
|
||||
let pkce = if provider.use_pkce_in_device_flow() {
|
||||
let random_bytes: [u8; 32] = rand::random::<[u8; 32]>();
|
||||
let verifier = URL_SAFE_NO_PAD.encode(random_bytes);
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(verifier.as_bytes());
|
||||
let challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
|
||||
Some((verifier, challenge))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let scopes = provider.scopes();
|
||||
let mut device_params: Vec<(&str, &str)> = vec![("client_id", provider.client_id())];
|
||||
if !scopes.is_empty() {
|
||||
device_params.push(("scope", scopes.as_str()));
|
||||
}
|
||||
if let Some((_, ref challenge)) = pkce {
|
||||
device_params.push(("code_challenge", challenge.as_str()));
|
||||
device_params.push(("code_challenge_method", "S256"));
|
||||
}
|
||||
let form: HashMap<&str, &str> = device_params.iter().copied().collect();
|
||||
|
||||
let mut device_request = client.post(device_auth_url).form(&form);
|
||||
for (key, value) in provider.extra_token_headers() {
|
||||
device_request = device_request.header(key, value);
|
||||
}
|
||||
let device_response: Value = device_request.send().await?.json().await?;
|
||||
|
||||
let device_code = device_response["device_code"]
|
||||
.as_str()
|
||||
.ok_or_else(|| {
|
||||
anyhow!("Missing device_code in device authorization response: {device_response}")
|
||||
})?
|
||||
.to_string();
|
||||
let user_code = device_response["user_code"]
|
||||
.as_str()
|
||||
.ok_or_else(|| {
|
||||
anyhow!("Missing user_code in device authorization response: {device_response}")
|
||||
})?
|
||||
.to_string();
|
||||
let verification_uri = device_response["verification_uri"]
|
||||
.as_str()
|
||||
.ok_or_else(|| {
|
||||
anyhow!("Missing verification_uri in device authorization response: {device_response}")
|
||||
})?
|
||||
.to_string();
|
||||
let verification_uri_complete = device_response["verification_uri_complete"]
|
||||
.as_str()
|
||||
.map(|s| s.to_string());
|
||||
let expires_in = device_response["expires_in"].as_i64().unwrap_or(1800);
|
||||
let mut interval = device_response["interval"].as_u64().unwrap_or(5);
|
||||
let deadline = Utc::now().timestamp() + expires_in;
|
||||
|
||||
println!(
|
||||
"\nAuthenticate with {} (client '{}'):",
|
||||
provider.provider_name(),
|
||||
client_name
|
||||
);
|
||||
println!(" 1. Open: {verification_uri}");
|
||||
println!(" 2. Enter code: {user_code}\n");
|
||||
if let Some(complete) = &verification_uri_complete {
|
||||
println!(" (Or open the pre-filled URL: {complete})\n");
|
||||
}
|
||||
let url_to_open = verification_uri_complete
|
||||
.as_deref()
|
||||
.unwrap_or(&verification_uri);
|
||||
|
||||
if std::env::var(crate::sandbox::SANDBOX_ENV_FLAG).is_ok()
|
||||
&& let Ok(qr) = qrcode::QrCode::new(url_to_open)
|
||||
{
|
||||
let rendered = qr
|
||||
.render::<qrcode::render::unicode::Dense1x2>()
|
||||
.quiet_zone(true)
|
||||
.build();
|
||||
println!("{rendered}\n");
|
||||
}
|
||||
|
||||
let _ = open::that(url_to_open);
|
||||
|
||||
println!("Waiting for authorization (polling every {interval}s)...\n");
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
|
||||
|
||||
if Utc::now().timestamp() >= deadline {
|
||||
bail!(
|
||||
"Device code expired before user approval. Run `coyote --authenticate {}` to try again.",
|
||||
client_name
|
||||
);
|
||||
}
|
||||
|
||||
let mut token_params: Vec<(&str, &str)> = vec![
|
||||
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
|
||||
("device_code", device_code.as_str()),
|
||||
("client_id", provider.client_id()),
|
||||
];
|
||||
if let Some((verifier, _)) = pkce.as_ref() {
|
||||
token_params.push(("code_verifier", verifier.as_str()));
|
||||
}
|
||||
let token_response: Value = build_token_request(&client, provider, &token_params)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
if let Some(access_token) = token_response["access_token"].as_str() {
|
||||
let refresh_token = token_response["refresh_token"].as_str().map(str::to_string);
|
||||
let expires_in_secs = token_response["expires_in"].as_i64().ok_or_else(|| {
|
||||
anyhow!("Missing expires_in in device_code token response: {token_response}")
|
||||
})?;
|
||||
let expires_at = Utc::now().timestamp() + expires_in_secs;
|
||||
let account_id = provider.extract_account_id(&token_response);
|
||||
|
||||
let tokens = OAuthTokens {
|
||||
access_token: access_token.to_string(),
|
||||
refresh_token,
|
||||
expires_at,
|
||||
account_id,
|
||||
};
|
||||
save_oauth_tokens(client_name, &tokens)?;
|
||||
println!(
|
||||
"Successfully authenticated client '{}' with {} via OAuth (device_code). Tokens saved.",
|
||||
client_name,
|
||||
provider.provider_name()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let error_code = token_response["error"].as_str().unwrap_or("");
|
||||
match error_code {
|
||||
"authorization_pending" => continue,
|
||||
"slow_down" => {
|
||||
interval += 5;
|
||||
println!("Server requested slower polling; increasing interval to {interval}s.");
|
||||
continue;
|
||||
}
|
||||
"expired_token" => bail!(
|
||||
"Device code expired. Run `coyote --authenticate {}` to try again.",
|
||||
client_name
|
||||
),
|
||||
"access_denied" => bail!("Authorization was denied by the user."),
|
||||
other => bail!(
|
||||
"Device code polling failed: {} — {}",
|
||||
other,
|
||||
token_response["error_description"]
|
||||
.as_str()
|
||||
.unwrap_or("no description")
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_oauth_tokens(client_name: &str) -> Option<OAuthTokens> {
|
||||
let path = paths::token_file(client_name);
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
@@ -426,7 +606,9 @@ pub async fn prepare_oauth_access_token(
|
||||
|
||||
let tokens = if Utc::now().timestamp() >= tokens.expires_at {
|
||||
match provider.flow() {
|
||||
OAuthFlow::Pkce => refresh_oauth_token(client, provider, client_name, &tokens).await?,
|
||||
OAuthFlow::Pkce | OAuthFlow::DeviceCode => {
|
||||
refresh_oauth_token(client, provider, client_name, &tokens).await?
|
||||
}
|
||||
OAuthFlow::ClientCredentials => {
|
||||
run_client_credentials_flow(provider, client_name).await?;
|
||||
load_oauth_tokens(client_name)
|
||||
@@ -664,12 +846,14 @@ mod tests {
|
||||
authorize_url: Some("https://base.example/authorize".into()),
|
||||
redirect_uri: None,
|
||||
redirect_port: Some(1234),
|
||||
device_authorization_url: None,
|
||||
scopes: vec!["a".into(), "b".into()],
|
||||
token_request_format: Some(TokenRequestFormat::FormUrlEncoded),
|
||||
extra_authorize_params: IndexMap::from([("plan".into(), "base".into())]),
|
||||
extra_token_headers: IndexMap::new(),
|
||||
extra_request_headers: IndexMap::new(),
|
||||
echo_pkce_in_token_exchange: false,
|
||||
use_pkce_in_device_flow: false,
|
||||
include_state_in_token_exchange: true,
|
||||
}
|
||||
}
|
||||
@@ -683,12 +867,14 @@ mod tests {
|
||||
authorize_url: None,
|
||||
redirect_uri: None,
|
||||
redirect_port: None,
|
||||
device_authorization_url: None,
|
||||
scopes: vec![],
|
||||
token_request_format: None,
|
||||
extra_authorize_params: IndexMap::new(),
|
||||
extra_token_headers: IndexMap::new(),
|
||||
extra_request_headers: IndexMap::new(),
|
||||
echo_pkce_in_token_exchange: false,
|
||||
use_pkce_in_device_flow: false,
|
||||
include_state_in_token_exchange: true,
|
||||
}
|
||||
}
|
||||
@@ -937,4 +1123,133 @@ echo_pkce_in_token_exchange: true
|
||||
assert!(provider.uses_localhost_redirect());
|
||||
assert!(provider.fixed_redirect_uri().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_flow_device_code_parses() {
|
||||
let yaml = "client_id: x\ntoken_url: y\nflow: device_code";
|
||||
|
||||
let cfg: OAuthConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert!(matches!(cfg.flow, OAuthFlow::DeviceCode));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_config_merge_preserves_device_authorization_url_when_user_omits() {
|
||||
let mut base = base_config();
|
||||
base.device_authorization_url = Some("https://base.example/device".into());
|
||||
let user = empty_user_override("user-id", "https://user.example/token");
|
||||
|
||||
let merged = base.merge(user);
|
||||
|
||||
assert_eq!(
|
||||
merged.device_authorization_url.as_deref(),
|
||||
Some("https://base.example/device")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_config_merge_user_device_authorization_url_wins() {
|
||||
let mut base = base_config();
|
||||
base.device_authorization_url = Some("https://base.example/device".into());
|
||||
let mut user = empty_user_override("user-id", "https://user.example/token");
|
||||
user.device_authorization_url = Some("https://user.example/device".into());
|
||||
|
||||
let merged = base.merge(user);
|
||||
|
||||
assert_eq!(
|
||||
merged.device_authorization_url.as_deref(),
|
||||
Some("https://user.example/device")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_config_merge_user_pkce_in_device_flow_wins() {
|
||||
let base = base_config();
|
||||
let mut user = empty_user_override("user-id", "https://user.example/token");
|
||||
user.use_pkce_in_device_flow = true;
|
||||
|
||||
let merged = base.merge(user);
|
||||
|
||||
assert!(merged.use_pkce_in_device_flow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_compatible_provider_exposes_device_authorization_url() {
|
||||
let mut cfg = base_config();
|
||||
cfg.device_authorization_url = Some("https://example/device".into());
|
||||
|
||||
let provider = OpenAICompatibleOAuthProvider {
|
||||
config: cfg,
|
||||
client_name: "test".into(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
provider.device_authorization_url(),
|
||||
Some("https://example/device")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_compatible_provider_device_authorization_url_none_when_unset() {
|
||||
let provider = OpenAICompatibleOAuthProvider {
|
||||
config: base_config(),
|
||||
client_name: "test".into(),
|
||||
};
|
||||
|
||||
assert!(provider.device_authorization_url().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_compatible_provider_use_pkce_in_device_flow_defaults_false() {
|
||||
let provider = OpenAICompatibleOAuthProvider {
|
||||
config: base_config(),
|
||||
client_name: "test".into(),
|
||||
};
|
||||
|
||||
assert!(!provider.use_pkce_in_device_flow());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_compatible_provider_use_pkce_in_device_flow_returns_true_when_set() {
|
||||
let mut cfg = base_config();
|
||||
cfg.use_pkce_in_device_flow = true;
|
||||
|
||||
let provider = OpenAICompatibleOAuthProvider {
|
||||
config: cfg,
|
||||
client_name: "test".into(),
|
||||
};
|
||||
|
||||
assert!(provider.use_pkce_in_device_flow());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oauth_config_serde_roundtrip_device_code_yaml() {
|
||||
let yaml = r#"
|
||||
client_id: my-client
|
||||
token_url: https://auth.example/oauth/token
|
||||
device_authorization_url: https://auth.example/oauth/device_authorization
|
||||
flow: device_code
|
||||
token_request_format: form_url_encoded
|
||||
use_pkce_in_device_flow: true
|
||||
scopes:
|
||||
- read
|
||||
- write
|
||||
"#;
|
||||
|
||||
let cfg: OAuthConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert_eq!(cfg.client_id, "my-client");
|
||||
assert_eq!(cfg.token_url, "https://auth.example/oauth/token");
|
||||
assert_eq!(
|
||||
cfg.device_authorization_url.as_deref(),
|
||||
Some("https://auth.example/oauth/device_authorization")
|
||||
);
|
||||
assert!(matches!(cfg.flow, OAuthFlow::DeviceCode));
|
||||
assert!(matches!(
|
||||
cfg.token_request_format,
|
||||
Some(TokenRequestFormat::FormUrlEncoded)
|
||||
));
|
||||
assert!(cfg.use_pkce_in_device_flow);
|
||||
assert_eq!(cfg.scopes, vec!["read", "write"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,4 +89,12 @@ impl OAuthProvider for OpenAICompatibleOAuthProvider {
|
||||
fn echo_pkce_in_token_exchange(&self) -> bool {
|
||||
self.config.echo_pkce_in_token_exchange
|
||||
}
|
||||
|
||||
fn device_authorization_url(&self) -> Option<&str> {
|
||||
self.config.device_authorization_url.as_deref()
|
||||
}
|
||||
|
||||
fn use_pkce_in_device_flow(&self) -> bool {
|
||||
self.config.use_pkce_in_device_flow
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -221,7 +221,7 @@ impl Agent {
|
||||
&& !matches!(agent_config.memory, Some(false))
|
||||
&& !matches!(app.memory, Some(false))
|
||||
{
|
||||
let memory_exists = paths::global_memory_index_path().exists()
|
||||
let memory_exists = paths::global_memory_index_file().exists()
|
||||
|| env::current_dir()
|
||||
.ok()
|
||||
.and_then(|cwd| memory::discover_workspace_memory(&cwd))
|
||||
|
||||
@@ -329,7 +329,7 @@ impl AppConfig {
|
||||
return path.clone();
|
||||
}
|
||||
|
||||
if let Some(translated) = paths::translate_sandboxed_home_path(path)
|
||||
if let Some(translated) = paths::translate_sandboxed_home_dir(path)
|
||||
&& translated.exists()
|
||||
{
|
||||
info!(
|
||||
@@ -380,7 +380,7 @@ impl AppConfig {
|
||||
let theme = if self.highlight {
|
||||
let theme_mode = if self.light_theme() { "light" } else { "dark" };
|
||||
let theme_filename = format!("{theme_mode}.tmTheme");
|
||||
let theme_path = paths::local_path(&theme_filename);
|
||||
let theme_path = paths::local_dir(&theme_filename);
|
||||
if theme_path.exists() {
|
||||
let theme = ThemeSet::get_theme(&theme_path)
|
||||
.with_context(|| format!("Invalid theme at '{}'", theme_path.display()))?;
|
||||
|
||||
+1
-1
@@ -481,7 +481,7 @@ fn confirm_asset_overwrite(category: AssetCategory, label: &str, target: &Path)
|
||||
pub fn default_sessions_dir() -> PathBuf {
|
||||
match env::var(get_env_name("sessions_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => paths::local_path(SESSIONS_DIR_NAME),
|
||||
Err(_) => paths::local_dir(SESSIONS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+43
-43
@@ -30,11 +30,11 @@ pub fn config_dir() -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_path(name: &str) -> PathBuf {
|
||||
pub fn local_dir(name: &str) -> PathBuf {
|
||||
config_dir().join(name)
|
||||
}
|
||||
|
||||
pub fn cache_path() -> PathBuf {
|
||||
pub fn cache_dir() -> PathBuf {
|
||||
if let Ok(v) = env::var(get_env_name("cache_dir")) {
|
||||
PathBuf::from(v)
|
||||
} else if let Ok(v) = env::var("XDG_CACHE_HOME") {
|
||||
@@ -49,7 +49,7 @@ pub fn sandbox_kit_override() -> Option<PathBuf> {
|
||||
env::var_os(get_env_name("sandbox_kit")).map(PathBuf::from)
|
||||
}
|
||||
|
||||
pub fn translate_sandboxed_home_path(path: &Path) -> Option<PathBuf> {
|
||||
pub fn translate_sandboxed_home_dir(path: &Path) -> Option<PathBuf> {
|
||||
env::var_os("IS_SANDBOX")?;
|
||||
|
||||
let s = path.to_str()?;
|
||||
@@ -62,7 +62,7 @@ pub fn translate_sandboxed_home_path(path: &Path) -> Option<PathBuf> {
|
||||
return Some(translated);
|
||||
}
|
||||
|
||||
translate_windows_users_path(s)
|
||||
translate_windows_users_dir(s)
|
||||
}
|
||||
|
||||
fn translate_unix_home_style(s: &str, prefix: &str) -> Option<PathBuf> {
|
||||
@@ -83,7 +83,7 @@ fn translate_unix_home_style(s: &str, prefix: &str) -> Option<PathBuf> {
|
||||
})
|
||||
}
|
||||
|
||||
fn translate_windows_users_path(s: &str) -> Option<PathBuf> {
|
||||
fn translate_windows_users_dir(s: &str) -> Option<PathBuf> {
|
||||
let bytes = s.as_bytes();
|
||||
if bytes.len() < 4 || !bytes[0].is_ascii_alphabetic() || bytes[1] != b':' || bytes[2] != b'\\' {
|
||||
return None;
|
||||
@@ -128,20 +128,20 @@ pub fn find_workspace_sbx_mixin(start: &Path) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn oauth_tokens_path() -> PathBuf {
|
||||
cache_path().join("oauth")
|
||||
pub fn oauth_tokens_dir() -> PathBuf {
|
||||
cache_dir().join("oauth")
|
||||
}
|
||||
|
||||
pub fn token_file(client_name: &str) -> PathBuf {
|
||||
oauth_tokens_path().join(format!("{client_name}_oauth_tokens.json"))
|
||||
oauth_tokens_dir().join(format!("{client_name}_oauth_tokens.json"))
|
||||
}
|
||||
|
||||
pub fn log_path() -> PathBuf {
|
||||
cache_path().join(format!("{}.log", env!("CARGO_CRATE_NAME")))
|
||||
pub fn log_file() -> PathBuf {
|
||||
cache_dir().join(format!("{}.log", env!("CARGO_CRATE_NAME")))
|
||||
}
|
||||
|
||||
pub fn sbx_kit_dir() -> PathBuf {
|
||||
cache_path().join(SBX_KIT_DIR_NAME)
|
||||
cache_dir().join(SBX_KIT_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn sbx_kit_hash_file() -> PathBuf {
|
||||
@@ -149,7 +149,7 @@ pub fn sbx_kit_hash_file() -> PathBuf {
|
||||
}
|
||||
|
||||
pub fn sbx_vault_mixins_dir() -> PathBuf {
|
||||
cache_path().join(SBX_VAULT_MIXINS_DIR_NAME)
|
||||
cache_dir().join(SBX_VAULT_MIXINS_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn sbx_vault_mixins_hash_file() -> PathBuf {
|
||||
@@ -157,20 +157,20 @@ pub fn sbx_vault_mixins_hash_file() -> PathBuf {
|
||||
}
|
||||
|
||||
pub fn sbx_mixin_kits_dir() -> PathBuf {
|
||||
cache_path().join(SBX_MIXIN_KITS_DIR_NAME)
|
||||
cache_dir().join(SBX_MIXIN_KITS_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn config_file() -> PathBuf {
|
||||
match env::var(get_env_name("config_file")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(CONFIG_FILE_NAME),
|
||||
Err(_) => local_dir(CONFIG_FILE_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn roles_dir() -> PathBuf {
|
||||
match env::var(get_env_name("roles_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(ROLES_DIR_NAME),
|
||||
Err(_) => local_dir(ROLES_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ pub fn role_file(name: &str) -> PathBuf {
|
||||
pub fn skills_dir() -> PathBuf {
|
||||
match env::var(get_env_name("skills_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(SKILLS_DIR_NAME),
|
||||
Err(_) => local_dir(SKILLS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ pub fn validate_skill_name(name: &str) -> Result<()> {
|
||||
pub fn macros_dir() -> PathBuf {
|
||||
match env::var(get_env_name("macros_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(MACROS_DIR_NAME),
|
||||
Err(_) => local_dir(MACROS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,21 +254,21 @@ pub fn macro_file(name: &str) -> PathBuf {
|
||||
pub fn env_file() -> PathBuf {
|
||||
match env::var(get_env_name("env_file")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(ENV_FILE_NAME),
|
||||
Err(_) => local_dir(ENV_FILE_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rags_dir() -> PathBuf {
|
||||
match env::var(get_env_name("rags_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(RAGS_DIR_NAME),
|
||||
Err(_) => local_dir(RAGS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn functions_dir() -> PathBuf {
|
||||
match env::var(get_env_name("functions_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => local_path(FUNCTIONS_DIR_NAME),
|
||||
Err(_) => local_dir(FUNCTIONS_DIR_NAME),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ pub fn bash_prompt_utils_file() -> PathBuf {
|
||||
}
|
||||
|
||||
pub fn agents_data_dir() -> PathBuf {
|
||||
local_path(AGENTS_DIR_NAME)
|
||||
local_dir(AGENTS_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn agent_data_dir(name: &str) -> PathBuf {
|
||||
@@ -339,14 +339,14 @@ pub fn agent_functions_file(name: &str) -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
pub fn models_override_file() -> PathBuf {
|
||||
local_path("models-override.yaml")
|
||||
local_dir("models-override.yaml")
|
||||
}
|
||||
|
||||
pub fn global_memory_dir() -> PathBuf {
|
||||
config_dir().join(MEMORY_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn global_memory_index_path() -> PathBuf {
|
||||
pub fn global_memory_index_file() -> PathBuf {
|
||||
global_memory_dir().join(MEMORY_INDEX_FILE_NAME)
|
||||
}
|
||||
|
||||
@@ -356,12 +356,12 @@ pub fn workspace_memory_dir_for(workspace_root: &Path) -> PathBuf {
|
||||
.join(MEMORY_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn workspace_memory_index_path_for(workspace_root: &Path) -> PathBuf {
|
||||
pub fn workspace_memory_index_file_for(workspace_root: &Path) -> PathBuf {
|
||||
workspace_memory_dir_for(workspace_root).join(MEMORY_INDEX_FILE_NAME)
|
||||
}
|
||||
|
||||
pub fn repl_history_dir() -> PathBuf {
|
||||
cache_path().join(REPL_HISTORY_DIR_NAME)
|
||||
cache_dir().join(REPL_HISTORY_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn repl_history_file(session: &Option<Session>) -> PathBuf {
|
||||
@@ -384,7 +384,7 @@ pub fn log_config() -> Result<(LevelFilter, Option<PathBuf>)> {
|
||||
});
|
||||
let resolved_log_path = match env::var(get_env_name("log_path")) {
|
||||
Ok(v) => Some(PathBuf::from(v)),
|
||||
Err(_) => Some(log_path()),
|
||||
Err(_) => Some(log_file()),
|
||||
};
|
||||
Ok((log_level, resolved_log_path))
|
||||
}
|
||||
@@ -571,7 +571,7 @@ mod tests {
|
||||
fn returns_none_when_not_in_sandbox() {
|
||||
without_sandbox(|| {
|
||||
let p = Path::new("/home/atusa/.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
assert_eq!(translate_sandboxed_home_dir(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -581,7 +581,7 @@ mod tests {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/home/atusa/.coyote_password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
translate_sandboxed_home_dir(p),
|
||||
Some(PathBuf::from("/home/agent/.coyote_password"))
|
||||
);
|
||||
});
|
||||
@@ -589,11 +589,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn translates_nested_host_home_path() {
|
||||
fn translates_nested_host_home_dir() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/home/atusa/.config/coyote/.password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
translate_sandboxed_home_dir(p),
|
||||
Some(PathBuf::from("/home/agent/.config/coyote/.password"))
|
||||
);
|
||||
});
|
||||
@@ -604,7 +604,7 @@ mod tests {
|
||||
fn returns_none_when_path_already_targets_agent_home() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/home/agent/.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
assert_eq!(translate_sandboxed_home_dir(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -613,7 +613,7 @@ mod tests {
|
||||
fn returns_none_when_path_is_outside_home() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/etc/coyote/.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
assert_eq!(translate_sandboxed_home_dir(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -622,7 +622,7 @@ mod tests {
|
||||
fn returns_none_for_relative_path() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new(".coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
assert_eq!(translate_sandboxed_home_dir(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -631,17 +631,17 @@ mod tests {
|
||||
fn returns_none_for_first_segment_not_home() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/opt/atusa/.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
assert_eq!(translate_sandboxed_home_dir(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn translates_macos_users_path() {
|
||||
fn translates_macos_users_dir() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/Users/atusa/.coyote_password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
translate_sandboxed_home_dir(p),
|
||||
Some(PathBuf::from("/home/agent/.coyote_password"))
|
||||
);
|
||||
});
|
||||
@@ -649,11 +649,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn translates_macos_nested_path() {
|
||||
fn translates_macos_nested_dir() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/Users/atusa/.config/coyote/.password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
translate_sandboxed_home_dir(p),
|
||||
Some(PathBuf::from("/home/agent/.config/coyote/.password"))
|
||||
);
|
||||
});
|
||||
@@ -661,10 +661,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_none_when_macos_path_already_targets_agent() {
|
||||
fn returns_none_when_macos_dir_already_targets_agent() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/Users/agent/.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
assert_eq!(translate_sandboxed_home_dir(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -674,7 +674,7 @@ mod tests {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("C:\\Users\\atusa\\.coyote_password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
translate_sandboxed_home_dir(p),
|
||||
Some(PathBuf::from("/home/agent/.coyote_password"))
|
||||
);
|
||||
});
|
||||
@@ -686,7 +686,7 @@ mod tests {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("D:\\Users\\atusa\\.config\\coyote\\.password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
translate_sandboxed_home_dir(p),
|
||||
Some(PathBuf::from("/home/agent/.config/coyote/.password"))
|
||||
);
|
||||
});
|
||||
@@ -697,7 +697,7 @@ mod tests {
|
||||
fn returns_none_when_windows_path_already_targets_agent() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("C:\\Users\\agent\\.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
assert_eq!(translate_sandboxed_home_dir(p), None);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,9 +440,9 @@ impl RequestContext {
|
||||
match &self.agent {
|
||||
None => match env::var(get_env_name("messages_file")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => paths::cache_path().join(MESSAGES_FILE_NAME),
|
||||
Err(_) => paths::cache_dir().join(MESSAGES_FILE_NAME),
|
||||
},
|
||||
Some(agent) => paths::cache_path()
|
||||
Some(agent) => paths::cache_dir()
|
||||
.join(AGENTS_DIR_NAME)
|
||||
.join(agent.name())
|
||||
.join(MESSAGES_FILE_NAME),
|
||||
@@ -453,7 +453,7 @@ impl RequestContext {
|
||||
match &self.agent {
|
||||
None => match env::var(get_env_name("sessions_dir")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
Err(_) => paths::local_path(SESSIONS_DIR_NAME),
|
||||
Err(_) => paths::local_dir(SESSIONS_DIR_NAME),
|
||||
},
|
||||
Some(agent) => paths::agent_data_dir(agent.name()).join(SESSIONS_DIR_NAME),
|
||||
}
|
||||
@@ -927,7 +927,7 @@ impl RequestContext {
|
||||
let store = cwd.as_deref().map(MemoryStore::new);
|
||||
let workspace = store.as_ref().and_then(|s| s.workspace.clone());
|
||||
|
||||
let global_exists = paths::global_memory_index_path().exists();
|
||||
let global_exists = paths::global_memory_index_file().exists();
|
||||
let workspace_exists = workspace.is_some();
|
||||
|
||||
if !global_exists && !workspace_exists {
|
||||
@@ -5609,7 +5609,7 @@ mod tests {
|
||||
#[serial]
|
||||
fn use_session_creates_temp_session() {
|
||||
let _guard = TestConfigDirGuard::new();
|
||||
let sessions_dir = paths::local_path("sessions");
|
||||
let sessions_dir = paths::local_dir("sessions");
|
||||
create_dir_all(&sessions_dir).unwrap();
|
||||
|
||||
let mut ctx = create_test_ctx();
|
||||
@@ -5625,7 +5625,7 @@ mod tests {
|
||||
#[serial]
|
||||
fn use_session_creates_named_session() {
|
||||
let _guard = TestConfigDirGuard::new();
|
||||
let sessions_dir = paths::local_path("sessions");
|
||||
let sessions_dir = paths::local_dir("sessions");
|
||||
create_dir_all(&sessions_dir).unwrap();
|
||||
|
||||
let mut ctx = create_test_ctx();
|
||||
@@ -5641,7 +5641,7 @@ mod tests {
|
||||
#[serial]
|
||||
fn exit_session_roundtrip() {
|
||||
let _guard = TestConfigDirGuard::new();
|
||||
let sessions_dir = paths::local_path("sessions");
|
||||
let sessions_dir = paths::local_dir("sessions");
|
||||
create_dir_all(&sessions_dir).unwrap();
|
||||
|
||||
let mut ctx = create_test_ctx();
|
||||
|
||||
@@ -321,7 +321,7 @@ pub fn handle_memory_tool(ctx: &mut RequestContext, cmd_name: &str, args: &Value
|
||||
|
||||
Ok(json!({
|
||||
"files": entries,
|
||||
"global_index_exists": paths::global_memory_index_path().exists(),
|
||||
"global_index_exists": paths::global_memory_index_file().exists(),
|
||||
"workspace": store.workspace.as_ref().map(workspace_label),
|
||||
}))
|
||||
}
|
||||
|
||||
+2
-2
@@ -388,14 +388,14 @@ async fn run(
|
||||
if let Some(scope) = cli.init_memory {
|
||||
let (path, content) = match scope {
|
||||
MemoryScope::Global => (
|
||||
paths::global_memory_index_path(),
|
||||
paths::global_memory_index_file(),
|
||||
"# Global Memory\n\n<!-- Universal facts about you go here. The LLM uses this as always-on context. -->\n<!-- Drill files (when created) are listed below. -->\n",
|
||||
),
|
||||
MemoryScope::Workspace => {
|
||||
let cwd = env::current_dir()?;
|
||||
let root = memory::find_git_root(&cwd).unwrap_or(cwd);
|
||||
(
|
||||
paths::workspace_memory_index_path_for(&root),
|
||||
paths::workspace_memory_index_file_for(&root),
|
||||
"# Workspace Memory Index\n\n<!-- Facts about this project go here. The LLM uses this as always-on context. -->\n<!-- Drill files (when created) are listed below. -->\n",
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -142,7 +142,7 @@ fn mcp_token_key(server_name: &str) -> String {
|
||||
}
|
||||
|
||||
fn load_registered_client_id(server_name: &str) -> Option<String> {
|
||||
let path = paths::oauth_tokens_path().join(format!("mcp_{server_name}_registration.json"));
|
||||
let path = paths::oauth_tokens_dir().join(format!("mcp_{server_name}_registration.json"));
|
||||
let content = fs::read_to_string(path).ok()?;
|
||||
let reg: McpRegistration = serde_json::from_str(&content).ok()?;
|
||||
|
||||
@@ -150,7 +150,7 @@ fn load_registered_client_id(server_name: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
fn save_registered_client_id(server_name: &str, client_id: &str) -> Result<()> {
|
||||
let dir = paths::oauth_tokens_path();
|
||||
let dir = paths::oauth_tokens_dir();
|
||||
fs::create_dir_all(&dir)?;
|
||||
|
||||
let path = dir.join(format!("mcp_{server_name}_registration.json"));
|
||||
|
||||
@@ -388,6 +388,26 @@ fn copy_host_files(name: &str) -> Result<()> {
|
||||
);
|
||||
}
|
||||
|
||||
let oauth_tokens_dir = paths::oauth_tokens_dir();
|
||||
if oauth_tokens_dir.exists() {
|
||||
let sandbox_oauth_dir = "/home/agent/.cache/coyote/oauth";
|
||||
ensure_sandbox_dir(name, sandbox_oauth_dir)?;
|
||||
let dest = format!("{name}:{sandbox_oauth_dir}/");
|
||||
for entry in fs::read_dir(&oauth_tokens_dir)
|
||||
.with_context(|| format!("Failed to read {}", oauth_tokens_dir.display()))?
|
||||
{
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
sbx_cp(&path.display().to_string(), &dest)?;
|
||||
}
|
||||
chown_agent_recursive(name, sandbox_oauth_dir)?;
|
||||
} else {
|
||||
debug!(
|
||||
"Skipping OAuth token copy: {} does not exist",
|
||||
oauth_tokens_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
match resolve_vault_password_file() {
|
||||
Some(password_file) if password_file.exists() => {
|
||||
let dest_path = host_to_sandbox_path(&password_file, &home_dir, cfg!(windows))?;
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ use tokio::time::sleep;
|
||||
|
||||
pub async fn tail_logs(no_color: bool) {
|
||||
let re = Regex::new(r"^(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+<(?P<opid>[^\s>]+)>\s+\[(?P<level>[A-Z]+)\]\s+(?P<logger>[^:]+):(?P<line>\d+)\s+-\s+(?P<message>.*)$").unwrap();
|
||||
let file_path = paths::log_path();
|
||||
let file_path = paths::log_file();
|
||||
let file = File::open(&file_path).expect("Cannot open file");
|
||||
let mut reader = BufReader::new(file);
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ fn apply_sandboxed_home_translation(provider_def: &mut LocalProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(translated) = paths::translate_sandboxed_home_path(pf) else {
|
||||
let Some(translated) = paths::translate_sandboxed_home_dir(pf) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user