integration fixes

This commit is contained in:
jaberjaber23
2026-05-12 16:13:08 +03:00
parent 7f7b071528
commit 4c496be02f
6 changed files with 119 additions and 23 deletions
+1 -1
View File
@@ -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"
+2 -2
View File
@@ -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>
+9 -9
View File
@@ -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 -1
View File
@@ -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": {
+22 -10
View File
@@ -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());
}
}
}
}
+84
View File
@@ -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();