mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 06:32:17 +00:00
integration fixes
This commit is contained in:
+1
-1
@@ -18,7 +18,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.6.7"
|
||||
version = "0.6.8"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0 OR MIT"
|
||||
repository = "https://github.com/RightNow-AI/openfang"
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/language-Rust-orange?style=flat-square" alt="Rust" />
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="MIT" />
|
||||
<img src="https://img.shields.io/badge/version-0.6.7-green?style=flat-square" alt="v0.6.7" />
|
||||
<img src="https://img.shields.io/badge/tests-2,657%2B%20passing-brightgreen?style=flat-square" alt="Tests" />
|
||||
<img src="https://img.shields.io/badge/version-0.6.8-green?style=flat-square" alt="v0.6.8" />
|
||||
<img src="https://img.shields.io/badge/tests-2,669%2B%20passing-brightgreen?style=flat-square" alt="Tests" />
|
||||
<img src="https://img.shields.io/badge/clippy-0%20warnings-brightgreen?style=flat-square" alt="Clippy" />
|
||||
<a href="https://www.buymeacoffee.com/openfang" target="_blank"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-FFDD00?style=flat-square&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me A Coffee" /></a>
|
||||
</p>
|
||||
|
||||
@@ -1457,7 +1457,7 @@ mod tests {
|
||||
async fn test_parse_image_only_no_caption() {
|
||||
let bot_id = Arc::new(RwLock::new(Some("bot123".to_string())));
|
||||
let d = payload_with("", vec![att("photo.png", Some("image/png"), 100_000)]);
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true)
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true, &empty_threads())
|
||||
.await
|
||||
.unwrap();
|
||||
match msg.content {
|
||||
@@ -1480,7 +1480,7 @@ mod tests {
|
||||
"look at this",
|
||||
vec![att("photo.jpg", Some("image/jpeg"), 50_000)],
|
||||
);
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true)
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true, &empty_threads())
|
||||
.await
|
||||
.unwrap();
|
||||
match msg.content {
|
||||
@@ -1512,7 +1512,7 @@ mod tests {
|
||||
att("b.png", Some("image/png"), 20_000),
|
||||
],
|
||||
);
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true)
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true, &empty_threads())
|
||||
.await
|
||||
.unwrap();
|
||||
match msg.content {
|
||||
@@ -1536,7 +1536,7 @@ mod tests {
|
||||
att("b.png", Some("image/png"), 20_000),
|
||||
],
|
||||
);
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true)
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true, &empty_threads())
|
||||
.await
|
||||
.unwrap();
|
||||
match msg.content {
|
||||
@@ -1555,7 +1555,7 @@ mod tests {
|
||||
async fn test_parse_heic_falls_to_file() {
|
||||
let bot_id = Arc::new(RwLock::new(Some("bot123".to_string())));
|
||||
let d = payload_with("", vec![att("photo.heic", Some("image/heic"), 100_000)]);
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true)
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true, &empty_threads())
|
||||
.await
|
||||
.unwrap();
|
||||
match msg.content {
|
||||
@@ -1575,7 +1575,7 @@ mod tests {
|
||||
"",
|
||||
vec![att("huge.png", Some("image/png"), 6 * 1024 * 1024)],
|
||||
);
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true)
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true, &empty_threads())
|
||||
.await
|
||||
.unwrap();
|
||||
match msg.content {
|
||||
@@ -1600,7 +1600,7 @@ mod tests {
|
||||
"see attached",
|
||||
vec![att("doc.pdf", Some("application/pdf"), 200_000)],
|
||||
);
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true)
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true, &empty_threads())
|
||||
.await
|
||||
.unwrap();
|
||||
match msg.content {
|
||||
@@ -1619,7 +1619,7 @@ mod tests {
|
||||
// we should fall back to the filename extension.
|
||||
let bot_id = Arc::new(RwLock::new(Some("bot123".to_string())));
|
||||
let d = payload_with("", vec![att("pic.png", None, 50_000)]);
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true)
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true, &empty_threads())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(msg.content, ChannelContent::Image { .. }));
|
||||
@@ -1629,7 +1629,7 @@ mod tests {
|
||||
async fn test_parse_empty_message_with_no_attachments_returns_none() {
|
||||
let bot_id = Arc::new(RwLock::new(Some("bot123".to_string())));
|
||||
let d = payload_with("", vec![]);
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true).await;
|
||||
let msg = parse_discord_message(&d, &bot_id, &[], &[], true, &empty_threads()).await;
|
||||
assert!(msg.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpenFang",
|
||||
"version": "0.6.7",
|
||||
"version": "0.6.8",
|
||||
"identifier": "ai.openfang.desktop",
|
||||
"build": {},
|
||||
"app": {
|
||||
|
||||
@@ -5249,12 +5249,20 @@ impl OpenFangKernel {
|
||||
.iter()
|
||||
.map(|p| p.id.clone())
|
||||
.collect();
|
||||
let env_map: std::collections::HashMap<String, String> = catalog
|
||||
.list_providers()
|
||||
.iter()
|
||||
.filter(|p| !p.api_key_env.is_empty())
|
||||
.map(|p| (p.api_key_env.to_ascii_uppercase(), p.id.clone()))
|
||||
.collect();
|
||||
// Multi-valued: several providers may share the same api_key_env
|
||||
// (e.g. both `openai` and `codex` use OPENAI_API_KEY). Using a
|
||||
// plain HashMap silently dropped earlier providers — broke #1188.
|
||||
let mut env_map: std::collections::HashMap<String, Vec<String>> =
|
||||
std::collections::HashMap::new();
|
||||
for p in catalog.list_providers() {
|
||||
if p.api_key_env.is_empty() {
|
||||
continue;
|
||||
}
|
||||
env_map
|
||||
.entry(p.api_key_env.to_ascii_uppercase())
|
||||
.or_default()
|
||||
.push(p.id.clone());
|
||||
}
|
||||
(ids, env_map)
|
||||
};
|
||||
|
||||
@@ -5407,10 +5415,12 @@ impl OpenFangKernel {
|
||||
}
|
||||
for var in skill.manifest.config.values() {
|
||||
if let Some(env_name) = var.env.as_deref() {
|
||||
if let Some(provider) =
|
||||
if let Some(providers) =
|
||||
env_to_provider.get(&env_name.to_ascii_uppercase())
|
||||
{
|
||||
set.insert(provider.clone());
|
||||
for provider in providers {
|
||||
set.insert(provider.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5422,10 +5432,12 @@ impl OpenFangKernel {
|
||||
// wired that provider into their MCP server.
|
||||
for server in &self.config.mcp_servers {
|
||||
for env_name in &server.env {
|
||||
if let Some(provider) =
|
||||
if let Some(providers) =
|
||||
env_to_provider.get(&env_name.to_ascii_uppercase())
|
||||
{
|
||||
set.insert(provider.clone());
|
||||
for provider in providers {
|
||||
set.insert(provider.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,45 @@ pub fn enforce_require_signed(
|
||||
)));
|
||||
}
|
||||
|
||||
// Bind the signature to the installed bytes.
|
||||
//
|
||||
// envelope.verify() only proves the envelope's signature matches its own
|
||||
// embedded `manifest` text. Without comparing that text to the actual
|
||||
// skill.toml / SKILL.md on disk, an attacker could ship a benign signed
|
||||
// envelope alongside malicious skill files and pass the check.
|
||||
//
|
||||
// Read every candidate manifest file in the installed dir and require
|
||||
// that at least one byte-matches envelope.manifest.
|
||||
const MANIFEST_CANDIDATES: &[&str] = &["skill.toml", "SKILL.md", "skill.md"];
|
||||
let mut bound = false;
|
||||
for name in MANIFEST_CANDIDATES {
|
||||
let path = skill_dir.join(name);
|
||||
if !path.exists() {
|
||||
continue;
|
||||
}
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(actual) if actual == envelope.manifest => {
|
||||
bound = true;
|
||||
break;
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
return Err(SkillError::SecurityBlocked(format!(
|
||||
"require_signed: failed to read {} for binding check: {e}",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !bound {
|
||||
return Err(SkillError::SecurityBlocked(format!(
|
||||
"require_signed: signed envelope content does not match any \
|
||||
installed manifest file in {} (signature was valid but the \
|
||||
skill payload on disk differs from what was signed)",
|
||||
skill_dir.display()
|
||||
)));
|
||||
}
|
||||
|
||||
if !opts.allowed_signer_keys.is_empty() {
|
||||
let actual = hex::encode(&envelope.signer_public_key);
|
||||
let actual_lower = actual.to_lowercase();
|
||||
@@ -278,6 +317,51 @@ entry = "main.py"
|
||||
}
|
||||
}
|
||||
|
||||
/// Critical: a valid signature for a different manifest must NOT pass
|
||||
/// when the on-disk skill.toml differs. Without binding the envelope to
|
||||
/// the installed bytes, an attacker could ship a benign signed envelope
|
||||
/// next to malicious skill files.
|
||||
#[test]
|
||||
fn require_signed_on_rejects_signature_unbound_to_disk() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
// Sign a BENIGN manifest body but never write that text to disk.
|
||||
let benign_toml = r#"name = "benign"
|
||||
version = "0.1.0"
|
||||
description = "Looks fine."
|
||||
|
||||
[runtime]
|
||||
type = "python"
|
||||
entry = "main.py"
|
||||
"#;
|
||||
let signing_key = SigningKey::generate(&mut OsRng);
|
||||
let envelope =
|
||||
SignedManifest::sign(benign_toml.to_string(), &signing_key, "trusted-signer");
|
||||
write_signature(dir.path(), &envelope, "signature.json");
|
||||
|
||||
// Write a DIFFERENT (malicious) skill.toml on disk.
|
||||
let evil_toml = r#"name = "evil"
|
||||
version = "0.1.0"
|
||||
description = "Backdoor."
|
||||
|
||||
[runtime]
|
||||
type = "python"
|
||||
entry = "rm-rf.py"
|
||||
"#;
|
||||
std::fs::write(dir.path().join("skill.toml"), evil_toml).unwrap();
|
||||
|
||||
let opts = InstallOptions::require_signed();
|
||||
let err = enforce_require_signed(dir.path(), &opts).unwrap_err();
|
||||
match err {
|
||||
SkillError::SecurityBlocked(msg) => {
|
||||
assert!(
|
||||
msg.contains("does not match any") || msg.contains("payload on disk differs"),
|
||||
"got: {msg}"
|
||||
);
|
||||
}
|
||||
other => panic!("expected SecurityBlocked, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_signature_returns_none_when_absent() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user