mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-27 08:00:17 +00:00
feat(extensions): improve auth UX and add load-time validation (#536)
* feat(extensions): add load-time validation for auth capabilities Catch common misconfigurations (missing auth section, missing setup_url, short prompts) at startup via tracing::warn instead of silently failing at auth time. * feat(extensions): improve auth prompts, setup_url, and showAuthCard Add setup_url and descriptive prompts to channel and tool capabilities files. Fix showAuthCard in web gateway and improve extension manager auth flow messaging. * refactor(extensions): extract MIN_PROMPT_LENGTH constant in validate() Address review feedback: replace magic number 30 with a named constant for readability and maintainability. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cbcd5adcc0
commit
13697976db
@@ -105,6 +105,59 @@ impl CapabilitiesFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Validate the capabilities file and emit warnings for common misconfigurations.
|
||||
///
|
||||
/// Called once at load time to catch issues early. Warnings are emitted via
|
||||
/// `tracing::warn` so they show up in startup logs without blocking loading.
|
||||
pub fn validate(&self, name: &str) {
|
||||
const MIN_PROMPT_LENGTH: usize = 30;
|
||||
|
||||
// setup.required_secrets present but no auth section → auth card won't display
|
||||
if let Some(setup) = &self.setup {
|
||||
if !setup.required_secrets.is_empty() && self.auth.is_none() {
|
||||
tracing::warn!(
|
||||
tool = name,
|
||||
"setup.required_secrets defined but no 'auth' section — \
|
||||
chat-based auth card will not display for this tool"
|
||||
);
|
||||
}
|
||||
|
||||
// Check for short prompts
|
||||
for secret in &setup.required_secrets {
|
||||
if secret.prompt.len() < MIN_PROMPT_LENGTH {
|
||||
tracing::warn!(
|
||||
tool = name,
|
||||
secret = secret.name,
|
||||
prompt = secret.prompt,
|
||||
"setup.required_secrets prompt is shorter than {} chars — \
|
||||
consider a more descriptive prompt that tells the user where to find this value",
|
||||
MIN_PROMPT_LENGTH
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manual auth (no OAuth) checks
|
||||
if let Some(auth) = &self.auth
|
||||
&& auth.oauth.is_none()
|
||||
{
|
||||
if auth.setup_url.is_none() {
|
||||
tracing::warn!(
|
||||
tool = name,
|
||||
"auth section has no OAuth and no setup_url — \
|
||||
user has no link to obtain credentials"
|
||||
);
|
||||
}
|
||||
if auth.instructions.is_none() {
|
||||
tracing::warn!(
|
||||
tool = name,
|
||||
"auth section has no OAuth and no instructions — \
|
||||
user has no guidance on how to obtain credentials"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to runtime Capabilities.
|
||||
pub fn to_capabilities(&self) -> Capabilities {
|
||||
let mut caps = Capabilities::default();
|
||||
@@ -1056,6 +1109,60 @@ mod tests {
|
||||
assert_eq!(caps.setup.unwrap().required_secrets[0].name, "my_secret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_setup_without_auth_warns() {
|
||||
// setup.required_secrets with no auth section — should not panic
|
||||
let json = r#"{
|
||||
"setup": {
|
||||
"required_secrets": [
|
||||
{ "name": "api_key", "prompt": "Enter your API key from the provider dashboard settings page" }
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let caps = CapabilitiesFile::from_json(json).unwrap();
|
||||
// Should not panic; warning is emitted via tracing
|
||||
caps.validate("test-tool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_manual_auth_missing_fields() {
|
||||
// auth without OAuth, missing setup_url and instructions
|
||||
let json = r#"{
|
||||
"auth": {
|
||||
"secret_name": "my_api_key"
|
||||
}
|
||||
}"#;
|
||||
|
||||
let caps = CapabilitiesFile::from_json(json).unwrap();
|
||||
// Should not panic; warnings emitted for missing setup_url and instructions
|
||||
caps.validate("test-tool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_clean_tool() {
|
||||
// Well-configured tool with auth, setup_url, instructions, and good prompts
|
||||
let json = r#"{
|
||||
"auth": {
|
||||
"secret_name": "my_api_key",
|
||||
"setup_url": "https://example.com/api-keys",
|
||||
"instructions": "Go to example.com/api-keys and create a new key"
|
||||
},
|
||||
"setup": {
|
||||
"required_secrets": [
|
||||
{
|
||||
"name": "my_api_key",
|
||||
"prompt": "Enter your API key from https://example.com/api-keys"
|
||||
}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let caps = CapabilitiesFile::from_json(json).unwrap();
|
||||
// Should not panic and emits no warnings (has auth, setup_url, instructions, long prompt)
|
||||
caps.validate("clean-tool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_nested_empty_capabilities_noop() {
|
||||
// Empty inner capabilities should not clobber outer http
|
||||
|
||||
@@ -126,6 +126,7 @@ impl WasmToolLoader {
|
||||
let cap_bytes = fs::read(cap_path).await?;
|
||||
let cap_file = CapabilitiesFile::from_bytes(&cap_bytes)
|
||||
.map_err(|e| WasmLoadError::InvalidCapabilities(e.to_string()))?;
|
||||
cap_file.validate(name);
|
||||
let caps = cap_file.to_capabilities();
|
||||
let oauth = resolve_oauth_refresh_config(&cap_file);
|
||||
(caps, oauth)
|
||||
|
||||
Reference in New Issue
Block a user