Skip to content

Commit 39393e9

Browse files
committed
fix(sandbox): stage opencode copilot auth in cluster mode
1 parent 8e92b1a commit 39393e9

File tree

8 files changed

+537
-20
lines changed

8 files changed

+537
-20
lines changed

crates/openshell-providers/src/context.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
pub trait DiscoveryContext {
55
fn env_var(&self, key: &str) -> Option<String>;
6+
fn read_file(&self, path: &std::path::Path) -> Option<String>;
67
}
78

89
pub struct RealDiscoveryContext;
@@ -11,4 +12,8 @@ impl DiscoveryContext for RealDiscoveryContext {
1112
fn env_var(&self, key: &str) -> Option<String> {
1213
std::env::var(key).ok()
1314
}
15+
16+
fn read_file(&self, path: &std::path::Path) -> Option<String> {
17+
std::fs::read_to_string(path).ok()
18+
}
1419
}

crates/openshell-providers/src/providers/github.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
use crate::{
55
discover_with_spec, ProviderDiscoverySpec, ProviderError, ProviderPlugin, RealDiscoveryContext,
66
};
7+
use std::path::PathBuf;
78

89
pub struct GithubProvider;
10+
const OPENCODE_AUTH_JSON_CREDENTIAL_KEY: &str = "OPENCODE_AUTH_JSON";
911

1012
pub const SPEC: ProviderDiscoverySpec = ProviderDiscoverySpec {
1113
id: "github",
@@ -18,14 +20,40 @@ impl ProviderPlugin for GithubProvider {
1820
}
1921

2022
fn discover_existing(&self) -> Result<Option<crate::DiscoveredProvider>, ProviderError> {
21-
discover_with_spec(&SPEC, &RealDiscoveryContext)
23+
discover_existing_with_context(&RealDiscoveryContext)
2224
}
2325

2426
fn credential_env_vars(&self) -> &'static [&'static str] {
2527
SPEC.credential_env_vars
2628
}
2729
}
2830

31+
fn discover_existing_with_context(
32+
context: &dyn crate::DiscoveryContext,
33+
) -> Result<Option<crate::DiscoveredProvider>, ProviderError> {
34+
let mut discovered = discover_with_spec(&SPEC, context)?.unwrap_or_default();
35+
36+
let auth_path = context
37+
.env_var("HOME")
38+
.map(PathBuf::from)
39+
.unwrap_or_else(|| PathBuf::from("/"))
40+
.join(".local/share/opencode/auth.json");
41+
42+
if let Some(contents) = context.read_file(&auth_path)
43+
&& !contents.trim().is_empty()
44+
{
45+
discovered
46+
.credentials
47+
.insert(OPENCODE_AUTH_JSON_CREDENTIAL_KEY.to_string(), contents);
48+
}
49+
50+
if discovered.is_empty() {
51+
Ok(None)
52+
} else {
53+
Ok(Some(discovered))
54+
}
55+
}
56+
2957
#[cfg(test)]
3058
mod tests {
3159
use super::SPEC;
@@ -75,4 +103,25 @@ mod tests {
75103
])
76104
);
77105
}
106+
107+
#[test]
108+
fn discovers_opencode_auth_json_as_additional_github_credential() {
109+
let ctx = MockDiscoveryContext::new()
110+
.with_env("HOME", "/home/alice")
111+
.with_file(
112+
"/home/alice/.local/share/opencode/auth.json",
113+
r#"{"github-copilot":{"type":"oauth","access":"tok","refresh":"tok","expires":0}}"#,
114+
);
115+
116+
let discovered = super::discover_existing_with_context(&ctx)
117+
.expect("discovery")
118+
.expect("provider");
119+
120+
assert_eq!(
121+
discovered
122+
.credentials
123+
.get(super::OPENCODE_AUTH_JSON_CREDENTIAL_KEY),
124+
Some(&r#"{"github-copilot":{"type":"oauth","access":"tok","refresh":"tok","expires":0}}"#.to_string())
125+
);
126+
}
78127
}

crates/openshell-providers/src/test_helpers.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
use crate::DiscoveryContext;
55
use std::collections::HashMap;
6+
use std::path::{Path, PathBuf};
67

78
#[derive(Default)]
89
pub struct MockDiscoveryContext {
910
env: HashMap<String, String>,
11+
files: HashMap<PathBuf, String>,
1012
}
1113

1214
impl MockDiscoveryContext {
@@ -18,10 +20,19 @@ impl MockDiscoveryContext {
1820
self.env.insert(key.to_string(), value.to_string());
1921
self
2022
}
23+
24+
pub fn with_file(mut self, path: &str, value: &str) -> Self {
25+
self.files.insert(PathBuf::from(path), value.to_string());
26+
self
27+
}
2128
}
2229

2330
impl DiscoveryContext for MockDiscoveryContext {
2431
fn env_var(&self, key: &str) -> Option<String> {
2532
self.env.get(key).cloned()
2633
}
34+
35+
fn read_file(&self, path: &Path) -> Option<String> {
36+
self.files.get(path).cloned()
37+
}
2738
}

crates/openshell-sandbox/src/child_env.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const LOCAL_NO_PROXY: &str = "127.0.0.1,localhost,::1";
88
const OPENCODE_AUTH_RELATIVE_PATH: &str = ".local/share/opencode/auth.json";
99
const SANDBOX_XDG_DATA_HOME: &str = "/sandbox/.local/share";
1010
const SANDBOX_OPENCODE_AUTH_PATH: &str = "/sandbox/.local/share/opencode/auth.json";
11+
const OPENCODE_AUTH_JSON_CREDENTIAL_KEY: &str = "OPENCODE_AUTH_JSON";
1112

1213
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1314
pub(crate) enum ToolAdapter {
@@ -65,8 +66,22 @@ pub(crate) fn tool_runtime_env_vars(
6566
}
6667
}
6768

69+
pub(crate) fn suppressed_provider_env_keys(
70+
command: &[String],
71+
provider_env: &std::collections::HashMap<String, String>,
72+
) -> &'static [&'static str] {
73+
match detect_tool_adapter(command) {
74+
Some(ToolAdapter::OpenCode) if is_github_copilot_targeted(provider_env) => {
75+
&["GITHUB_TOKEN", "GH_TOKEN"]
76+
}
77+
_ => &[],
78+
}
79+
}
80+
6881
fn is_github_copilot_targeted(provider_env: &std::collections::HashMap<String, String>) -> bool {
69-
provider_env.contains_key("GITHUB_TOKEN") || provider_env.contains_key("GH_TOKEN")
82+
provider_env.contains_key("GITHUB_TOKEN")
83+
|| provider_env.contains_key("GH_TOKEN")
84+
|| provider_env.contains_key(OPENCODE_AUTH_JSON_CREDENTIAL_KEY)
7085
}
7186

7287
pub(crate) fn proxy_env_vars(proxy_url: &str) -> [(&'static str, String); 9] {
@@ -229,6 +244,27 @@ mod tests {
229244
assert_eq!(tool_runtime_env_vars(&command, &provider_env), None);
230245
}
231246

247+
#[test]
248+
fn opencode_auth_json_only_requests_auth_projection_and_xdg_override() {
249+
let command = vec!["opencode".to_string(), "run".to_string()];
250+
let provider_env = std::collections::HashMap::from([(
251+
"OPENCODE_AUTH_JSON".to_string(),
252+
r#"{"github-copilot":{"type":"oauth"}}"#.to_string(),
253+
)]);
254+
255+
assert_eq!(
256+
auth_file_projection(&command, Path::new("/home/alice"), &provider_env),
257+
Some((
258+
PathBuf::from("/home/alice/.local/share/opencode/auth.json"),
259+
PathBuf::from("/sandbox/.local/share/opencode/auth.json"),
260+
))
261+
);
262+
assert_eq!(
263+
tool_runtime_env_vars(&command, &provider_env),
264+
Some([("XDG_DATA_HOME", "/sandbox/.local/share".to_string())])
265+
);
266+
}
267+
232268
#[test]
233269
fn opencode_without_github_provider_does_not_request_auth_projection_or_xdg_override() {
234270
let command = vec!["opencode".to_string(), "run".to_string()];
@@ -240,4 +276,28 @@ mod tests {
240276
);
241277
assert_eq!(tool_runtime_env_vars(&command, &provider_env), None);
242278
}
279+
280+
#[test]
281+
fn opencode_github_path_suppresses_placeholder_github_env_keys() {
282+
let command = vec!["opencode".to_string(), "run".to_string()];
283+
let provider_env =
284+
std::collections::HashMap::from([("GITHUB_TOKEN".to_string(), "ghu-test".to_string())]);
285+
286+
assert_eq!(
287+
suppressed_provider_env_keys(&command, &provider_env),
288+
["GITHUB_TOKEN", "GH_TOKEN"]
289+
);
290+
}
291+
292+
#[test]
293+
fn unsupported_tools_do_not_suppress_provider_env_keys() {
294+
let command = vec!["python".to_string(), "script.py".to_string()];
295+
let provider_env =
296+
std::collections::HashMap::from([("GITHUB_TOKEN".to_string(), "ghu-test".to_string())]);
297+
298+
assert_eq!(
299+
suppressed_provider_env_keys(&command, &provider_env),
300+
&[] as &[&str]
301+
);
302+
}
243303
}

0 commit comments

Comments
 (0)