Skip to content
Open
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
2 changes: 1 addition & 1 deletion crates/forge_api/src/forge_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ impl<
.credential
.as_ref()
.and_then(|c| match &c.auth_details {
forge_domain::AuthDetails::ApiKey(key) => Some(key.as_str()),
forge_domain::AuthDetails::ApiKey(provider) => Some(provider.api_key().as_str()),
_ => None,
})
{
Expand Down
2 changes: 1 addition & 1 deletion crates/forge_app/src/command_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ mod tests {
url_params: vec![],
credential: Some(AuthCredential {
id: ProviderId::OPENAI,
auth_details: AuthDetails::ApiKey("test-key".to_string().into()),
auth_details: AuthDetails::static_api_key("test-key".to_string().into()),
url_params: Default::default(),
}),
custom_headers: None,
Expand Down
2 changes: 1 addition & 1 deletion crates/forge_app/src/dto/openai/transformers/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ mod tests {
fn make_credential(provider_id: ProviderId, key: &str) -> Option<forge_domain::AuthCredential> {
Some(forge_domain::AuthCredential {
id: provider_id,
auth_details: forge_domain::AuthDetails::ApiKey(forge_domain::ApiKey::from(
auth_details: forge_domain::AuthDetails::static_api_key(forge_domain::ApiKey::from(
key.to_string(),
)),
url_params: HashMap::new(),
Expand Down
27 changes: 27 additions & 0 deletions crates/forge_config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ pub struct ProviderEntry {
/// Environment variable holding the API key for this provider.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key_var: Option<String>,
/// Shell command that produces an API key on stdout. When set, the
/// command is executed instead of reading a static key from an environment
/// variable. Falls back to `{api_key_var}_HELPER` env var when absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key_helper: Option<String>,
/// URL template for chat completions; may contain `{{VAR}}` placeholders
/// that are substituted from the credential's url params.
pub url: String,
Expand Down Expand Up @@ -353,4 +358,26 @@ mod tests {

assert_eq!(actual.temperature, fixture.temperature);
}

#[test]
fn test_provider_entry_api_key_helper_round_trip() {
let fixture = ForgeConfig {
providers: vec![ProviderEntry {
id: "test_provider".to_string(),
url: "https://api.example.com/v1/chat".to_string(),
api_key_helper: Some("vault read -field=token secret/key".to_string()),
..Default::default()
}],
..Default::default()
};

let toml = toml_edit::ser::to_string_pretty(&fixture).unwrap();
let actual = ConfigReader::default().read_toml(&toml).build().unwrap();

assert_eq!(actual.providers.len(), 1);
assert_eq!(
actual.providers[0].api_key_helper,
Some("vault read -field=token secret/key".to_string())
);
}
}
26 changes: 25 additions & 1 deletion crates/forge_domain/src/auth/auth_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ pub struct ApiKeyRequest {
pub struct ApiKeyResponse {
pub api_key: ApiKey,
pub url_params: HashMap<URLParam, URLParamValue>,
/// When set, the API key was produced by this shell command and the
/// credential should be stored as a [`HelperCommand`](super::ApiKeyProvider::HelperCommand).
pub helper_command: Option<String>,
}

// Authorization Code Flow
Expand Down Expand Up @@ -95,7 +98,7 @@ pub enum AuthContextResponse {
}

impl AuthContextResponse {
/// Creates an API key authentication context
/// Creates an API key authentication context with a static key.
pub fn api_key(
request: ApiKeyRequest,
api_key: impl ToString,
Expand All @@ -109,6 +112,27 @@ impl AuthContextResponse {
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
helper_command: None,
},
})
}

/// Creates an API key authentication context backed by a helper command.
pub fn api_key_with_helper(
request: ApiKeyRequest,
api_key: impl ToString,
url_params: HashMap<String, String>,
command: String,
) -> Self {
Self::ApiKey(AuthContext {
request,
response: ApiKeyResponse {
api_key: api_key.to_string().into(),
url_params: url_params
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
helper_command: Some(command),
},
})
}
Expand Down
Loading
Loading