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
23 changes: 18 additions & 5 deletions crates/freshell-runtime-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ pub enum ProviderSecretProfile {
CodexOnecliAuthFile,
OpencodeOnecliEnvironment,
OpencodeOnecliAuthFile,
OpencodeOnecliCaFile,
AmplifierOnecliEnvironment,
AmplifierOnecliKeysFile,
/// The approved OneCLI deployment: Amplifier's VLLM module talks to the
Expand All @@ -936,7 +937,9 @@ impl ProviderSecretProfile {
match self {
Self::ClaudeOnecliEnvironment | Self::ClaudeOnecliAuthFile => "claude",
Self::CodexOnecliEnvironment | Self::CodexOnecliAuthFile => "codex",
Self::OpencodeOnecliEnvironment | Self::OpencodeOnecliAuthFile => "opencode",
Self::OpencodeOnecliEnvironment
| Self::OpencodeOnecliAuthFile
| Self::OpencodeOnecliCaFile => "opencode",
Self::AmplifierOnecliEnvironment
| Self::AmplifierOnecliKeysFile
| Self::AmplifierOnecliLunarouteGlm53
Expand All @@ -953,6 +956,15 @@ impl ProviderSecretProfile {
_ => None,
}
}

/// Provider-home path for a OneCLI file grant, including non-auth files
/// such as the gateway CA certificate.
pub fn provider_file_relative_path(self) -> Option<&'static str> {
self.auth_relative_path().or(match self {
Self::OpencodeOnecliCaFile => Some(".config/onecli/gateway-ca.pem"),
_ => None,
})
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand Down Expand Up @@ -1473,28 +1485,29 @@ mod provider_launch_context_tests {
for (provider, profiles) in [
(
"claude",
[
vec![
ProviderSecretProfile::ClaudeOnecliEnvironment,
ProviderSecretProfile::ClaudeOnecliAuthFile,
],
),
(
"codex",
[
vec![
ProviderSecretProfile::CodexOnecliEnvironment,
ProviderSecretProfile::CodexOnecliAuthFile,
],
),
(
"opencode",
[
vec![
ProviderSecretProfile::OpencodeOnecliEnvironment,
ProviderSecretProfile::OpencodeOnecliAuthFile,
ProviderSecretProfile::OpencodeOnecliCaFile,
],
),
(
"amplifier",
[
vec![
ProviderSecretProfile::AmplifierOnecliEnvironment,
ProviderSecretProfile::AmplifierOnecliKeysFile,
],
Expand Down
73 changes: 71 additions & 2 deletions crates/freshell-server/src/managed_provider_bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,31 +301,39 @@ pub fn named_provider_onecli_references(
provider: &str,
) -> Result<Vec<ProviderSecretReference>, String> {
use freshell_runtime_protocol::ProviderSecretProfile;
let (prefix, environment, auth_file) = match provider {
let (prefix, environment, auth_file, ca_file) = match provider {
"claude" => (
"CLAUDE",
ProviderSecretProfile::ClaudeOnecliEnvironment,
ProviderSecretProfile::ClaudeOnecliAuthFile,
None,
),
"codex" => (
"CODEX",
ProviderSecretProfile::CodexOnecliEnvironment,
ProviderSecretProfile::CodexOnecliAuthFile,
None,
),
"opencode" => (
"OPENCODE",
ProviderSecretProfile::OpencodeOnecliEnvironment,
ProviderSecretProfile::OpencodeOnecliAuthFile,
Some(ProviderSecretProfile::OpencodeOnecliCaFile),
),
"amplifier" => (
"AMPLIFIER",
ProviderSecretProfile::AmplifierOnecliEnvironment,
ProviderSecretProfile::AmplifierOnecliKeysFile,
None,
),
_ => return Ok(Vec::new()),
};
let mut references = Vec::new();
for (suffix, profile) in [("ENV_FILE", environment), ("AUTH_FILE", auth_file)] {
let mut profiles = vec![("ENV_FILE", environment), ("AUTH_FILE", auth_file)];
if let Some(profile) = ca_file {
profiles.push(("CA_FILE", profile));
}
for (suffix, profile) in profiles {
let key = format!("FRESHELL_MANAGED_{prefix}_ONECLI_{suffix}");
if let Some(path) = std::env::var_os(&key).filter(|value| !value.is_empty()) {
let path = std::path::PathBuf::from(path);
Expand Down Expand Up @@ -356,6 +364,67 @@ mod tests {
use super::*;
use std::fs;

struct RestoreOnecliEnv(Vec<(&'static str, Option<std::ffi::OsString>)>);

impl Drop for RestoreOnecliEnv {
fn drop(&mut self) {
for (name, value) in self.0.drain(..) {
match value {
Some(value) => std::env::set_var(name, value),
None => std::env::remove_var(name),
}
}
}
}

#[test]
fn named_opencode_onecli_references_include_private_environment_auth_and_ca_files() {
let _env_lock = crate::test_env_lock::CLAUDE_ENV_TEST_LOCK.blocking_lock();
let workspace = tempfile::tempdir().unwrap();
let environment = workspace.path().join("opencode.env");
let auth = workspace.path().join("opencode-auth.json");
let ca = workspace.path().join("gateway-ca.pem");
for file in [&environment, &auth, &ca] {
fs::write(file, "fixture").unwrap();
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(file, fs::Permissions::from_mode(0o600)).unwrap();
}
let keys = [
"FRESHELL_MANAGED_OPENCODE_ONECLI_ENV_FILE",
"FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE",
"FRESHELL_MANAGED_OPENCODE_ONECLI_CA_FILE",
];
let _restore = RestoreOnecliEnv(keys.map(|key| (key, std::env::var_os(key))).into());
for (key, file) in keys.into_iter().zip([&environment, &auth, &ca]) {
std::env::set_var(key, file);
}

let references = named_provider_onecli_references("opencode").unwrap();

assert_eq!(
references
.iter()
.map(|reference| reference.profile)
.collect::<Vec<_>>(),
vec![
freshell_runtime_protocol::ProviderSecretProfile::OpencodeOnecliEnvironment,
freshell_runtime_protocol::ProviderSecretProfile::OpencodeOnecliAuthFile,
freshell_runtime_protocol::ProviderSecretProfile::OpencodeOnecliCaFile,
],
);
let actual_paths = references
.iter()
.map(|reference| reference.source_path.clone())
.collect::<Vec<_>>();
let expected_paths = [environment, auth, ca].map(|file| {
fs::canonicalize(file)
.unwrap()
.to_string_lossy()
.into_owned()
});
assert_eq!(actual_paths, expected_paths.to_vec());
}

#[test]
fn managed_claude_provider_context_renders_scoped_mcp_recipe() {
let workspace = tempfile::tempdir().unwrap();
Expand Down
10 changes: 8 additions & 2 deletions crates/freshell-server/src/settings_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2887,8 +2887,14 @@ mod tests {
}

fn uuid_like() -> String {
format!("{}-{:?}", std::process::id(), std::time::SystemTime::now())
.replace([':', '.', ' '], "-")
static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let sequence = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!(
"{}-{:?}-{sequence}",
std::process::id(),
std::time::SystemTime::now()
)
.replace([':', '.', ' '], "-")
}

// ── CFG-04: legacyLocalSettingsSeed ─────────────────────────────────────
Expand Down
20 changes: 10 additions & 10 deletions crates/freshell-session-host/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -857,16 +857,16 @@ async fn grant_execution(
format!("resolve managed provider credentials: {error}"),
)
})?;
prepare_provider_auth_files(
prepare_provider_files(
&terminal.mode,
&resolved_secrets.auth_files,
&resolved_secrets.provider_files,
terminal.run_as_uid,
terminal.run_as_gid,
)
.map_err(|error| {
RuntimeError::new(
RuntimeErrorCode::HostUnreachable,
format!("prepare OneCLI auth files: {error}"),
format!("prepare OneCLI provider files: {error}"),
)
})?;
let prepared = providers::prepare_terminal(terminal, &resolved_secrets.environment)
Expand Down Expand Up @@ -974,16 +974,16 @@ async fn grant_execution(
format!("resolve hosted OneCLI grant: {error}"),
)
})?;
prepare_provider_auth_files(
prepare_provider_files(
launch.provider.as_str(),
&resolved_secrets.auth_files,
&resolved_secrets.provider_files,
launch.run_as_uid,
launch.run_as_gid,
)
.map_err(|error| {
RuntimeError::new(
RuntimeErrorCode::HostUnreachable,
format!("prepare hosted OneCLI auth files: {error}"),
format!("prepare hosted OneCLI provider files: {error}"),
)
})?;
providers::prepare_fresh_agent_child_environment(
Expand Down Expand Up @@ -1671,17 +1671,17 @@ fn prepare_provider_context_at(
Ok(())
}

fn prepare_provider_auth_files(
fn prepare_provider_files(
provider: &str,
auth_files: &[(&str, Vec<u8>)],
provider_files: &[(&str, Vec<u8>)],
run_as_uid: u32,
run_as_gid: u32,
) -> Result<(), String> {
let auth_dir = Path::new("/tmp/freshell-provider-auth");
std::fs::create_dir_all(auth_dir).map_err(|error| error.to_string())?;
set_mode(auth_dir, 0o711)?;
let home = Path::new("/home/freshell/provider");
if !home.exists() && auth_files.is_empty() {
if !home.exists() && provider_files.is_empty() {
return Ok(());
}
let legacy_relative = match provider {
Expand All @@ -1705,7 +1705,7 @@ fn prepare_provider_auth_files(
],
)?;
}
for (index, (relative, contents)) in auth_files.iter().enumerate() {
for (index, (relative, contents)) in provider_files.iter().enumerate() {
let destination = home.join(relative);
let target = auth_dir.join(format!("provider-{index}"));
let temporary = auth_dir.join(format!("provider-{index}-tmp-{}", std::process::id()));
Expand Down
Loading
Loading