Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9308505
fix(install): always keep the SDK's build of the torch an engine pins
tomastola Aug 26, 2026
0669185
fix(install): keep the engine from undoing the SDK torch alignment
tomastola Aug 28, 2026
2492bfc
feat(install): let a user opt out of realigning torch
tomastola Aug 31, 2026
22fb6d8
docs(install): say why NotVerified is never fatal on its own
tomastola Aug 31, 2026
01fff76
test(e2e): assert the torch alignment actually ran on the reinstall
tomastola Aug 31, 2026
d616d14
fix(install): validate torch kernels before alignment
michaelroy-amd Aug 28, 2026
74a157a
fix(e2e): ensure reused runtimes have an engine
michaelroy-amd Aug 31, 2026
e57e5e4
test(e2e): assert the settled runtime is not called a violation
tomastola Sep 1, 2026
3ee2e4f
test(install): pin the dated-alpha manifest fallback
tomastola Sep 1, 2026
da2ea4c
fix(install): settle the torch of the runtime that owns the env
tomastola Sep 1, 2026
336709c
refactor(core): share one torch-alignment opt-out with the engine
tomastola Sep 1, 2026
c211792
fix(vllm): keep the SDK-stack hint under the torch-alignment opt-out
tomastola Sep 1, 2026
7b6f99f
fix(install): judge each dependency violation on its own subject
tomastola Sep 1, 2026
19f2da0
feat(install): report what the runtime could do before a realignment
tomastola Sep 1, 2026
4e38b21
perf(install): probe the host only when the runtime cannot serve
tomastola Sep 1, 2026
c0c1367
test(install): make the release-correction case actually test the rel…
tomastola Sep 1, 2026
83586fb
fix(e2e): repair a dangling active-runtime pointer in the pre-warm
tomastola Sep 1, 2026
05fb20b
docs(vllm): document the torch alignment and its opt-out
tomastola Sep 1, 2026
fd552ba
fix(security): keep provider credentials opaque
michaelroy-amd Sep 1, 2026
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
3 changes: 3 additions & 0 deletions apps/rocm/src/comfyui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,7 @@ mod tests {
],
..therock::RocmSdkPythonProbe::default()
}),
sdk_torch: None,
read_only: false,
imported_from: None,
installed_at_unix_ms: 100,
Expand Down Expand Up @@ -2176,6 +2177,7 @@ mod tests {
library_paths: vec![sdk_lib.clone()],
..Default::default()
}),
sdk_torch: None,
read_only: false,
imported_from: None,
installed_at_unix_ms: 100,
Expand Down Expand Up @@ -2272,6 +2274,7 @@ mod tests {
],
..therock::RocmSdkPythonProbe::default()
}),
sdk_torch: None,
read_only: false,
imported_from: None,
installed_at_unix_ms: 100,
Expand Down
4 changes: 2 additions & 2 deletions apps/rocm/src/dash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ fn chat_api_key_from_env() -> Option<String> {
/// unavailable store yields `None` (the dash still launches; switching to the
/// Anthropic provider then surfaces an actionable error turn).
fn anthropic_api_key_for_dash() -> Option<String> {
crate::provider_keys::resolve_provider_api_key("anthropic", "ANTHROPIC_API_KEY")
crate::provider_keys::provider_credential("anthropic", "ANTHROPIC_API_KEY")
.ok()
.map(|k| k.value)
.map(crate::provider_keys::ProviderCredential::into_value)
}

/// Adapt the built-in `rocm-core` model recipes into the TUI-local summaries the
Expand Down
10,257 changes: 6,310 additions & 3,947 deletions apps/rocm/src/main.rs

Large diffs are not rendered by default.

113 changes: 58 additions & 55 deletions apps/rocm/src/provider_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,23 @@ pub(crate) struct ProviderKeyStatus {
pub source: String,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct ProviderApiKey {
pub value: String,
pub source: String,
pub(crate) struct ProviderCredential(String);

impl ProviderCredential {
pub(crate) fn as_str(&self) -> &str {
&self.0
}

pub(crate) fn into_value(self) -> String {
self.0
}
}

pub(crate) trait ProviderKeyStore: Send + Sync {
fn label(&self) -> &'static str;
fn get_secret(&self, provider: &str) -> Result<Option<Vec<u8>>>;
fn set_secret(&self, provider: &str, secret: &[u8]) -> Result<()>;
fn clear_secret(&self, provider: &str) -> Result<()>;
fn get_entry(&self, provider: &str) -> Result<Option<Vec<u8>>>;
fn store_entry(&self, provider: &str, value: &[u8]) -> Result<()>;
fn remove_entry(&self, provider: &str) -> Result<()>;
}

#[derive(Debug, Clone, Copy, Default)]
Expand All @@ -49,9 +55,9 @@ pub(crate) fn provider_key_status(provider: &str, env_name: &str) -> ProviderKey
)
}

pub(crate) fn resolve_provider_api_key(provider: &str, env_name: &str) -> Result<ProviderApiKey> {
pub(crate) fn provider_credential(provider: &str, env_name: &str) -> Result<ProviderCredential> {
let store = NativeProviderKeyStore;
resolve_provider_api_key_with_store(
provider_credential_with_store(
&store,
provider,
env_name,
Expand All @@ -61,12 +67,12 @@ pub(crate) fn resolve_provider_api_key(provider: &str, env_name: &str) -> Result
)
}

pub(crate) fn set_provider_api_key(provider: &str, value: &str) -> Result<ProviderKeyStatus> {
pub(crate) fn store_provider_credential(provider: &str, value: &str) -> Result<ProviderKeyStatus> {
let store = NativeProviderKeyStore;
set_provider_api_key_with_store(&store, provider, value)
store_provider_credential_with_store(&store, provider, value)
}

pub(crate) fn set_provider_api_key_with_store(
pub(crate) fn store_provider_credential_with_store(
store: &dyn ProviderKeyStore,
provider: &str,
value: &str,
Expand All @@ -77,26 +83,26 @@ pub(crate) fn set_provider_api_key_with_store(
}
ensure_cloud_provider(provider)?;
store
.set_secret(provider, trimmed.as_bytes())
.store_entry(provider, trimmed.as_bytes())
.with_context(|| format!("failed to save {provider} API key in secure storage"))?;
Ok(ProviderKeyStatus {
state: ProviderKeyState::Configured,
source: secure_source_label(store.label()),
})
}

pub(crate) fn clear_provider_api_key(provider: &str) -> Result<ProviderKeyStatus> {
pub(crate) fn remove_provider_credential(provider: &str) -> Result<ProviderKeyStatus> {
let store = NativeProviderKeyStore;
clear_provider_api_key_with_store(&store, provider)
remove_provider_credential_with_store(&store, provider)
}

pub(crate) fn clear_provider_api_key_with_store(
pub(crate) fn remove_provider_credential_with_store(
store: &dyn ProviderKeyStore,
provider: &str,
) -> Result<ProviderKeyStatus> {
ensure_cloud_provider(provider)?;
store
.clear_secret(provider)
.remove_entry(provider)
.with_context(|| format!("failed to clear {provider} API key from secure storage"))?;
Ok(ProviderKeyStatus {
state: ProviderKeyState::Missing,
Expand Down Expand Up @@ -138,8 +144,8 @@ fn provider_key_status_with_store(
source: format!("env:{env_name}"),
};
}
match store.get_secret(provider) {
Ok(Some(secret)) if !secret.is_empty() => ProviderKeyStatus {
match store.get_entry(provider) {
Ok(Some(value)) if !value.is_empty() => ProviderKeyStatus {
state: ProviderKeyState::Configured,
source: secure_source_label(store.label()),
},
Expand All @@ -154,32 +160,26 @@ fn provider_key_status_with_store(
}
}

fn resolve_provider_api_key_with_store(
fn provider_credential_with_store(
store: &dyn ProviderKeyStore,
provider: &str,
env_name: &str,
env_value: Option<String>,
) -> Result<ProviderApiKey> {
) -> Result<ProviderCredential> {
ensure_cloud_provider(provider)?;
if let Some(value) = env_value {
return Ok(ProviderApiKey {
value,
source: format!("env:{env_name}"),
});
return Ok(ProviderCredential(value));
}
match store.get_secret(provider) {
Ok(Some(secret)) if !secret.is_empty() => {
let value = String::from_utf8(secret)
match store.get_entry(provider) {
Ok(Some(value)) if !value.is_empty() => {
let value = String::from_utf8(value)
.context("stored provider API key was not valid UTF-8")?
.trim()
.to_owned();
if value.is_empty() {
bail!("{provider} API key in secure storage is empty");
}
Ok(ProviderApiKey {
value,
source: secure_source_label(store.label()),
})
Ok(ProviderCredential(value))
}
Ok(_) => bail!(
"{provider} provider requires a saved API key; run `rocm config set-provider-key {provider}` or set {env_name} for this session"
Expand Down Expand Up @@ -207,21 +207,21 @@ impl ProviderKeyStore for NativeProviderKeyStore {
native_store_label()
}

fn get_secret(&self, provider: &str) -> Result<Option<Vec<u8>>> {
fn get_entry(&self, provider: &str) -> Result<Option<Vec<u8>>> {
with_native_entry(provider, |entry| match entry.get_secret() {
Ok(secret) => Ok(Some(secret)),
Ok(value) => Ok(Some(value)),
Err(KeyringError::NoEntry) => Ok(None),
Err(error) => Err(keyring_anyhow(error)),
})
}

fn set_secret(&self, provider: &str, secret: &[u8]) -> Result<()> {
fn store_entry(&self, provider: &str, value: &[u8]) -> Result<()> {
with_native_entry(provider, |entry| {
entry.set_secret(secret).map_err(keyring_anyhow)
entry.set_secret(value).map_err(keyring_anyhow)
})
}

fn clear_secret(&self, provider: &str) -> Result<()> {
fn remove_entry(&self, provider: &str) -> Result<()> {
with_native_entry(provider, |entry| match entry.delete_credential() {
Ok(()) | Err(KeyringError::NoEntry) => Ok(()),
Err(error) => Err(keyring_anyhow(error)),
Expand All @@ -236,7 +236,7 @@ impl ProviderKeyStore for NativeProviderKeyStore {
/// runtime *context* is already entered on the calling thread, that nested
/// `block_on` panics with "Cannot start a runtime from within a runtime". The
/// dash resolves keys off-runtime, but this is the single chokepoint for every
/// store op (get/set/clear) and for `resolve_provider_api_key` /
/// store op (get/store/remove) and for `provider_credential` /
/// `provider_key_status`, so guard the whole class here: when a runtime is
/// active, run the entry build *and* the action on a fresh OS thread that has no
/// runtime entered.
Expand Down Expand Up @@ -348,22 +348,22 @@ mod tests {
"test keychain"
}

fn get_secret(&self, provider: &str) -> Result<Option<Vec<u8>>> {
fn get_entry(&self, provider: &str) -> Result<Option<Vec<u8>>> {
if let Some(fail) = self.fail {
bail!("{fail}");
}
Ok(self.secrets.lock().unwrap().get(provider).cloned())
}

fn set_secret(&self, provider: &str, secret: &[u8]) -> Result<()> {
fn store_entry(&self, provider: &str, value: &[u8]) -> Result<()> {
self.secrets
.lock()
.unwrap()
.insert(provider.to_owned(), secret.to_vec());
.insert(provider.to_owned(), value.to_vec());
Ok(())
}

fn clear_secret(&self, provider: &str) -> Result<()> {
fn remove_entry(&self, provider: &str) -> Result<()> {
self.secrets.lock().unwrap().remove(provider);
Ok(())
}
Expand Down Expand Up @@ -392,19 +392,20 @@ mod tests {
}

#[test]
fn provider_key_store_round_trips_without_exposing_value_in_status() -> Result<()> {
fn provider_credential_round_trip_keeps_secret_out_of_status() -> Result<()> {
let store = MemoryKeyStore::default();
store.set_secret("openai", b"sk-secret-sentinel")?;
store_provider_credential_with_store(&store, "openai", "sk-secret-sentinel")?;

let status = provider_key_status_with_store(&store, "openai", "OPENAI_API_KEY", None);
let resolved =
resolve_provider_api_key_with_store(&store, "openai", "OPENAI_API_KEY", None)?;
let credential = provider_credential_with_store(&store, "openai", "OPENAI_API_KEY", None)?;

assert_eq!(status.state, ProviderKeyState::Configured);
assert_eq!(status.source, "secure:test keychain");
assert!(!provider_key_status_label(&status).contains("sk-secret"));
assert_eq!(resolved.value, "sk-secret-sentinel");
assert_eq!(resolved.source, "secure:test keychain");
assert_eq!(credential.as_str(), "sk-secret-sentinel");

remove_provider_credential_with_store(&store, "openai")?;
assert!(store.get_entry("openai")?.is_none());
Ok(())
}

Expand All @@ -415,9 +416,10 @@ mod tests {
..MemoryKeyStore::default()
};

let error = resolve_provider_api_key_with_store(&store, "openai", "OPENAI_API_KEY", None)
.unwrap_err()
.to_string();
let error = match provider_credential_with_store(&store, "openai", "OPENAI_API_KEY", None) {
Ok(_) => panic!("credential resolution should fail when storage is unavailable"),
Err(error) => error.to_string(),
};

assert!(error.contains("secure API-key storage is unavailable"));
assert!(error.contains("no plaintext fallback was used"));
Expand All @@ -428,9 +430,10 @@ mod tests {
let store = MemoryKeyStore::default();

let error =
resolve_provider_api_key_with_store(&store, "anthropic", "ANTHROPIC_API_KEY", None)
.unwrap_err()
.to_string();
match provider_credential_with_store(&store, "anthropic", "ANTHROPIC_API_KEY", None) {
Ok(_) => panic!("credential resolution should fail when no credential is stored"),
Err(error) => error.to_string(),
};

assert!(error.contains("requires a saved API key"));
assert!(error.contains("rocm config set-provider-key anthropic"));
Expand All @@ -455,7 +458,7 @@ mod tests {
let outcome = rt.block_on(async {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
// Read-only: never writes to a real keychain on dev machines.
NativeProviderKeyStore.get_secret("anthropic")
NativeProviderKeyStore.get_entry("anthropic")
}))
});
assert!(
Expand Down
25 changes: 13 additions & 12 deletions apps/rocm/src/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ struct LocalProvider<'a> {

struct RemoteProvider {
provider: &'static str,
api_key_env: &'static str,
credential_env: &'static str,
model_env: &'static str,
endpoint_env: &'static str,
default_endpoint: &'static str,
Expand Down Expand Up @@ -261,14 +261,14 @@ fn provider_adapter<'a>(
"local" => Box::new(LocalProvider { paths }),
"openai" => Box::new(RemoteProvider {
provider: "openai",
api_key_env: "OPENAI_API_KEY",
credential_env: "OPENAI_API_KEY",
model_env: "ROCM_CLI_OPENAI_MODEL",
endpoint_env: "OPENAI_BASE_URL",
default_endpoint: "https://api.openai.com/v1/chat/completions",
}),
"anthropic" => Box::new(RemoteProvider {
provider: "anthropic",
api_key_env: "ANTHROPIC_API_KEY",
credential_env: "ANTHROPIC_API_KEY",
model_env: "ROCM_CLI_ANTHROPIC_MODEL",
endpoint_env: "ANTHROPIC_BASE_URL",
default_endpoint: "https://api.anthropic.com/v1/messages",
Expand Down Expand Up @@ -366,7 +366,8 @@ impl ProviderAdapter for RemoteProvider {
.filter(|value| !value.trim().is_empty())
.into_iter()
.collect::<Vec<_>>();
let key_status = crate::provider_keys::provider_key_status(self.provider, self.api_key_env);
let key_status =
crate::provider_keys::provider_key_status(self.provider, self.credential_env);
Ok(ProviderStatus {
provider: self.provider.to_owned(),
auth_status: crate::provider_keys::provider_key_status_label(&key_status),
Expand All @@ -376,8 +377,8 @@ impl ProviderAdapter for RemoteProvider {
}

fn chat(&self, request: &ChatRequest) -> Result<ChatResponse> {
let api_key =
crate::provider_keys::resolve_provider_api_key(self.provider, self.api_key_env)?;
let credential =
crate::provider_keys::provider_credential(self.provider, self.credential_env)?;
let model = resolve_remote_model(self.provider, self.model_env, request.model.as_deref())?;
let endpoint = remote_endpoint(self.endpoint_env, self.default_endpoint);
let (content, tool_calls) = match self.provider {
Expand All @@ -386,7 +387,7 @@ impl ProviderAdapter for RemoteProvider {
let json = post_json_with_headers(
&endpoint,
&[
("Authorization", format!("Bearer {}", api_key.value)),
("Authorization", format!("Bearer {}", credential.as_str())),
("Content-Type", "application/json".to_owned()),
],
&body,
Expand All @@ -398,7 +399,7 @@ impl ProviderAdapter for RemoteProvider {
let json = post_json_with_headers(
&endpoint,
&[
("x-api-key", api_key.value),
("x-api-key", credential.into_value()),
("anthropic-version", "2023-06-01".to_owned()),
("Content-Type", "application/json".to_owned()),
],
Expand All @@ -421,8 +422,8 @@ impl ProviderAdapter for RemoteProvider {
request: &ChatRequest,
on_event: &mut dyn FnMut(ProviderStreamEvent) -> Result<()>,
) -> Result<ChatStreamSummary> {
let api_key =
crate::provider_keys::resolve_provider_api_key(self.provider, self.api_key_env)?;
let credential =
crate::provider_keys::provider_credential(self.provider, self.credential_env)?;
let model = resolve_remote_model(self.provider, self.model_env, request.model.as_deref())?;
let endpoint = remote_endpoint(self.endpoint_env, self.default_endpoint);
match self.provider {
Expand All @@ -432,7 +433,7 @@ impl ProviderAdapter for RemoteProvider {
stream_json_with_headers(
&endpoint,
&[
("Authorization", format!("Bearer {}", api_key.value)),
("Authorization", format!("Bearer {}", credential.as_str())),
("Content-Type", "application/json".to_owned()),
("Accept", "text/event-stream".to_owned()),
],
Expand All @@ -447,7 +448,7 @@ impl ProviderAdapter for RemoteProvider {
stream_json_with_headers(
&endpoint,
&[
("x-api-key", api_key.value),
("x-api-key", credential.into_value()),
("anthropic-version", "2023-06-01".to_owned()),
("Content-Type", "application/json".to_owned()),
("Accept", "text/event-stream".to_owned()),
Expand Down
Loading