mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 15:01:15 +00:00
cargo fmt
This commit is contained in:
@@ -3856,7 +3856,10 @@ pub async fn audit_append(
|
||||
};
|
||||
let outcome = req.outcome.clone().unwrap_or_else(|| "ok".to_string());
|
||||
|
||||
let hash = state.kernel.audit_log.record(agent_id, action, detail, outcome);
|
||||
let hash = state
|
||||
.kernel
|
||||
.audit_log
|
||||
.record(agent_id, action, detail, outcome);
|
||||
let seq = state.kernel.audit_log.len().saturating_sub(1) as u64;
|
||||
|
||||
(
|
||||
@@ -10063,7 +10066,9 @@ pub async fn clone_agent(
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": format!("Failed to serialize manifest: {e}")})),
|
||||
Json(
|
||||
serde_json::json!({"error": format!("Failed to serialize manifest: {e}")}),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -10240,7 +10245,12 @@ pub async fn list_agent_files(
|
||||
// Identity files live in the agent's private state directory (see #1097).
|
||||
// Fall back to the legacy workspace location for agents created before the
|
||||
// split so existing on-disk files remain reachable.
|
||||
let workspace = match entry.manifest.state_dir.as_ref().or(entry.manifest.workspace.as_ref()) {
|
||||
let workspace = match entry
|
||||
.manifest
|
||||
.state_dir
|
||||
.as_ref()
|
||||
.or(entry.manifest.workspace.as_ref())
|
||||
{
|
||||
Some(ws) => ws.clone(),
|
||||
None => {
|
||||
return (
|
||||
|
||||
@@ -167,8 +167,7 @@ mod tests {
|
||||
fn skill_install_request_defaults_back_compat() {
|
||||
// Existing callers send `{"name": "..."}` only. New optional fields
|
||||
// must default cleanly (issue #1170).
|
||||
let req: SkillInstallRequest =
|
||||
serde_json::from_str(r#"{"name":"github-helper"}"#).unwrap();
|
||||
let req: SkillInstallRequest = serde_json::from_str(r#"{"name":"github-helper"}"#).unwrap();
|
||||
assert_eq!(req.name, "github-helper");
|
||||
assert!(!req.require_signed);
|
||||
assert!(req.allowed_signer_keys.is_empty());
|
||||
|
||||
@@ -1048,7 +1048,9 @@ async fn handle_command(
|
||||
"command": cmd,
|
||||
"message": format!("Hand '{}' deactivated.", instance.hand_id),
|
||||
}),
|
||||
Err(e) => serde_json::json!({"type": "error", "content": format!("Stop failed: {e}")}),
|
||||
Err(e) => {
|
||||
serde_json::json!({"type": "error", "content": format!("Stop failed: {e}")})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match state.kernel.stop_agent_run(agent_id) {
|
||||
@@ -1058,7 +1060,9 @@ async fn handle_command(
|
||||
Ok(false) => {
|
||||
serde_json::json!({"type": "command_result", "command": cmd, "message": "No active run to cancel."})
|
||||
}
|
||||
Err(e) => serde_json::json!({"type": "error", "content": format!("Stop failed: {e}")}),
|
||||
Err(e) => {
|
||||
serde_json::json!({"type": "error", "content": format!("Stop failed: {e}")})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1812,10 +1816,7 @@ mod tests {
|
||||
// Cookie signed with the wrong secret must fail.
|
||||
let bad = crate::session_auth::create_session_token("alice", "other-secret", 1);
|
||||
let mut headers = axum::http::HeaderMap::new();
|
||||
headers.insert(
|
||||
"cookie",
|
||||
format!("openfang_session={bad}").parse().unwrap(),
|
||||
);
|
||||
headers.insert("cookie", format!("openfang_session={bad}").parse().unwrap());
|
||||
let uri = empty_uri();
|
||||
let ctx = WsAuthCtx {
|
||||
api_key: "secret",
|
||||
|
||||
@@ -1660,10 +1660,7 @@ async fn test_clone_agent_happy_path() {
|
||||
assert_eq!(manifest["description"], "Cloned for user 1");
|
||||
assert_eq!(
|
||||
manifest["tags"].as_array().unwrap(),
|
||||
&vec![
|
||||
serde_json::json!("clone"),
|
||||
serde_json::json!("user-1"),
|
||||
]
|
||||
&vec![serde_json::json!("clone"), serde_json::json!("user-1"),]
|
||||
);
|
||||
// Inherited from template — the system_prompt should match.
|
||||
assert_eq!(
|
||||
@@ -1678,10 +1675,7 @@ async fn test_clone_agent_happy_path() {
|
||||
.await
|
||||
.unwrap();
|
||||
let agents: Vec<serde_json::Value> = resp.json().await.unwrap();
|
||||
let names: Vec<&str> = agents
|
||||
.iter()
|
||||
.map(|a| a["name"].as_str().unwrap())
|
||||
.collect();
|
||||
let names: Vec<&str> = agents.iter().map(|a| a["name"].as_str().unwrap()).collect();
|
||||
assert!(names.contains(&"test-agent"));
|
||||
assert!(names.contains(&"cloned-user-1"));
|
||||
}
|
||||
@@ -1731,10 +1725,7 @@ async fn test_clone_agent_name_collision() {
|
||||
"duplicate name must return 409 Conflict"
|
||||
);
|
||||
let body: serde_json::Value = resp.json().await.unwrap();
|
||||
assert!(body["error"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("already exists"));
|
||||
assert!(body["error"].as_str().unwrap().contains("already exists"));
|
||||
|
||||
// Cloning into the template's own name must also be rejected.
|
||||
let resp = client
|
||||
|
||||
@@ -144,8 +144,7 @@ impl MatrixAdapter {
|
||||
&& status == reqwest::StatusCode::UNAUTHORIZED
|
||||
&& is_unknown_token_body(&body_text)
|
||||
{
|
||||
match try_refresh_tokens(&self.client, &self.homeserver_url, &self.tokens)
|
||||
.await
|
||||
match try_refresh_tokens(&self.client, &self.homeserver_url, &self.tokens).await
|
||||
{
|
||||
Ok(()) => {
|
||||
info!("Matrix: access token refreshed via MSC2918, retrying send");
|
||||
@@ -717,7 +716,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_is_unknown_token_body() {
|
||||
// Real matrix.org body for M_UNKNOWN_TOKEN under MAS.
|
||||
let body = r#"{"errcode":"M_UNKNOWN_TOKEN","error":"Token is not active","soft_logout":true}"#;
|
||||
let body =
|
||||
r#"{"errcode":"M_UNKNOWN_TOKEN","error":"Token is not active","soft_logout":true}"#;
|
||||
assert!(is_unknown_token_body(body));
|
||||
assert!(!is_hard_logout(body));
|
||||
|
||||
@@ -740,19 +740,17 @@ mod tests {
|
||||
// access and refresh tokens and returns the new pair.
|
||||
use axum::{routing::post, Json, Router};
|
||||
|
||||
async fn refresh_handler(
|
||||
Json(body): Json<serde_json::Value>,
|
||||
) -> Json<serde_json::Value> {
|
||||
let incoming = body
|
||||
.get("refresh_token")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
assert_eq!(incoming, "old_refresh");
|
||||
Json(serde_json::json!({
|
||||
"access_token": "new_access",
|
||||
"refresh_token": "new_refresh",
|
||||
"expires_in_ms": 3_600_000u64,
|
||||
}))
|
||||
async fn refresh_handler(Json(body): Json<serde_json::Value>) -> Json<serde_json::Value> {
|
||||
let incoming = body
|
||||
.get("refresh_token")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
assert_eq!(incoming, "old_refresh");
|
||||
Json(serde_json::json!({
|
||||
"access_token": "new_access",
|
||||
"refresh_token": "new_refresh",
|
||||
"expires_in_ms": 3_600_000u64,
|
||||
}))
|
||||
}
|
||||
|
||||
let app = Router::new().route("/_matrix/client/v3/refresh", post(refresh_handler));
|
||||
@@ -779,10 +777,7 @@ mod tests {
|
||||
drop(guard);
|
||||
|
||||
// Refresh with no refresh token configured must fail cleanly.
|
||||
let no_refresh: TokenPair = Arc::new(RwLock::new((
|
||||
Zeroizing::new("a".to_string()),
|
||||
None,
|
||||
)));
|
||||
let no_refresh: TokenPair = Arc::new(RwLock::new((Zeroizing::new("a".to_string()), None)));
|
||||
let err = try_refresh_tokens(&client, &homeserver, &no_refresh)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
@@ -722,7 +722,8 @@ mod tests {
|
||||
assert_eq!(resolved, None);
|
||||
|
||||
// Missing channel_id on the wire — no match (the binding is restrictive).
|
||||
let resolved = router.resolve_with_channel_id(&ChannelType::Discord, "any-user", None, None);
|
||||
let resolved =
|
||||
router.resolve_with_channel_id(&ChannelType::Discord, "any-user", None, None);
|
||||
assert_eq!(resolved, None);
|
||||
}
|
||||
|
||||
|
||||
@@ -329,9 +329,7 @@ impl ChannelAdapter for SlackAdapter {
|
||||
// connection during the rotation overlap. Ack on
|
||||
// both, but only forward to the agent once.
|
||||
if is_duplicate_envelope(&seen_envelopes, envelope_id) {
|
||||
debug!(
|
||||
"Slack: skipping duplicate envelope_id {envelope_id}"
|
||||
);
|
||||
debug!("Slack: skipping duplicate envelope_id {envelope_id}");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1234,9 +1234,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.channel, ChannelType::Telegram);
|
||||
assert_eq!(msg.sender.display_name, "Alice Smith");
|
||||
assert_eq!(msg.sender.platform_id, "111222333");
|
||||
@@ -1267,9 +1275,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The chat_id (used for replies) stays on sender.platform_id.
|
||||
assert_eq!(msg.sender.platform_id, "-1009876543210");
|
||||
@@ -1308,9 +1324,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tg_id = msg
|
||||
.metadata
|
||||
@@ -1345,9 +1369,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match &msg.content {
|
||||
ChannelContent::Command { name, args } => {
|
||||
assert_eq!(name, "agent");
|
||||
@@ -1379,8 +1411,16 @@ mod tests {
|
||||
let client = test_client();
|
||||
|
||||
// Empty allowed_users = allow all
|
||||
let msg =
|
||||
parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new()).await;
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await;
|
||||
assert!(msg.is_some());
|
||||
|
||||
// Non-matching allowed_users = filter out
|
||||
@@ -1434,9 +1474,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.channel, ChannelType::Telegram);
|
||||
assert_eq!(msg.sender.display_name, "Alice Smith");
|
||||
assert!(matches!(msg.content, ChannelContent::Text(ref t) if t == "Edited message!"));
|
||||
@@ -1472,9 +1520,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match &msg.content {
|
||||
ChannelContent::Command { name, args } => {
|
||||
assert_eq!(name, "agents");
|
||||
@@ -1498,9 +1554,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(msg.content, ChannelContent::Location { .. }));
|
||||
}
|
||||
|
||||
@@ -1524,9 +1588,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// With a fake token, getFile will fail, so we get a text fallback
|
||||
match &msg.content {
|
||||
ChannelContent::Text(t) => {
|
||||
@@ -1561,9 +1633,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match &msg.content {
|
||||
ChannelContent::Text(t) => {
|
||||
assert!(t.contains("Document received"));
|
||||
@@ -1594,9 +1674,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match &msg.content {
|
||||
ChannelContent::Text(t) => {
|
||||
assert!(t.contains("Voice message"));
|
||||
@@ -1627,9 +1715,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.thread_id, Some("42".to_string()));
|
||||
assert!(msg.is_group);
|
||||
}
|
||||
@@ -1649,9 +1745,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.thread_id, None);
|
||||
assert!(!msg.is_group);
|
||||
}
|
||||
@@ -1673,9 +1777,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.thread_id, Some("99".to_string()));
|
||||
}
|
||||
|
||||
@@ -1871,9 +1983,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg.sender.display_name, "My Channel");
|
||||
assert_eq!(msg.sender.platform_id, "-1001234567890");
|
||||
assert!(
|
||||
@@ -1895,8 +2015,16 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg =
|
||||
parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new()).await;
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await;
|
||||
assert!(msg.is_none());
|
||||
}
|
||||
|
||||
@@ -2162,9 +2290,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match &msg.content {
|
||||
ChannelContent::Text(t) => {
|
||||
assert!(t.starts_with("[Replying to Bob: We should use Rust]\n\n"));
|
||||
@@ -2204,9 +2340,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match &msg.content {
|
||||
ChannelContent::Text(t) => {
|
||||
assert!(t.starts_with("[Replying to Carol: Sunset view]\n\n"));
|
||||
@@ -2245,9 +2389,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match &msg.content {
|
||||
ChannelContent::Text(t) => {
|
||||
assert_eq!(t, "What was that?");
|
||||
@@ -2283,9 +2435,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match &msg.content {
|
||||
ChannelContent::Text(t) => {
|
||||
assert!(t.starts_with("[Replying to Unknown: Anonymous message]\n\n"));
|
||||
@@ -2310,9 +2470,17 @@ mod tests {
|
||||
});
|
||||
|
||||
let client = test_client();
|
||||
let msg = parse_telegram_update(&update, &[], "fake:token", &client, DEFAULT_API_URL, None, &HashMap::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = parse_telegram_update(
|
||||
&update,
|
||||
&[],
|
||||
"fake:token",
|
||||
&client,
|
||||
DEFAULT_API_URL,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match &msg.content {
|
||||
ChannelContent::Text(t) => {
|
||||
assert_eq!(t, "Just a normal message");
|
||||
|
||||
@@ -1086,11 +1086,10 @@ impl OpenFangKernel {
|
||||
openfang_runtime::media_understanding::MediaEngine::new(config.media.clone());
|
||||
// Closes #1051: thread MediaConfig URL overrides into the TTS engine
|
||||
// so local OpenAI/ElevenLabs-compatible services can be targeted.
|
||||
let tts_engine = openfang_runtime::tts::TtsEngine::new(config.tts.clone())
|
||||
.with_base_urls(
|
||||
config.media.tts_openai_base_url.clone(),
|
||||
config.media.tts_elevenlabs_base_url.clone(),
|
||||
);
|
||||
let tts_engine = openfang_runtime::tts::TtsEngine::new(config.tts.clone()).with_base_urls(
|
||||
config.media.tts_openai_base_url.clone(),
|
||||
config.media.tts_elevenlabs_base_url.clone(),
|
||||
);
|
||||
let mut pairing = crate::pairing::PairingManager::new(config.pairing.clone());
|
||||
|
||||
// Load paired devices from database and set up persistence callback
|
||||
@@ -1246,16 +1245,16 @@ impl OpenFangKernel {
|
||||
}
|
||||
|
||||
let audit_log_for_cb = Arc::clone(&kernel.audit_log);
|
||||
kernel.hand_registry.set_audit_callback(Arc::new(
|
||||
move |hand_id: &str, hash: &str| {
|
||||
kernel
|
||||
.hand_registry
|
||||
.set_audit_callback(Arc::new(move |hand_id: &str, hash: &str| {
|
||||
audit_log_for_cb.record(
|
||||
"kernel",
|
||||
openfang_runtime::audit::AuditAction::ConfigChange,
|
||||
format!("HAND.toml reload hand={hand_id} sha256={hash}"),
|
||||
"ok",
|
||||
);
|
||||
},
|
||||
));
|
||||
}));
|
||||
}
|
||||
|
||||
// Restore persisted agents from SQLite
|
||||
@@ -1541,9 +1540,7 @@ impl OpenFangKernel {
|
||||
}
|
||||
}
|
||||
if auto_spawned > 0 {
|
||||
info!(
|
||||
"Auto-spawned {auto_spawned} agent(s) from ~/.openfang/agents"
|
||||
);
|
||||
info!("Auto-spawned {auto_spawned} agent(s) from ~/.openfang/agents");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1705,7 +1702,10 @@ impl OpenFangKernel {
|
||||
// does not specify one. When the user sets `workspace = "/path"` in
|
||||
// agent.toml we leave that path alone — only data/, output/, skills/
|
||||
// get created lazily so private state never pollutes the target dir.
|
||||
let workspace_dir = manifest.workspace.clone().unwrap_or_else(|| state_dir.clone());
|
||||
let workspace_dir = manifest
|
||||
.workspace
|
||||
.clone()
|
||||
.unwrap_or_else(|| state_dir.clone());
|
||||
ensure_state_dir(&state_dir, &workspace_dir)?;
|
||||
ensure_workspace(&workspace_dir)?;
|
||||
if manifest.generate_identity_files {
|
||||
@@ -2127,7 +2127,10 @@ impl OpenFangKernel {
|
||||
// to the same path unless the manifest already pinned one. See #1097.
|
||||
if manifest.state_dir.is_none() {
|
||||
let state_dir = self.config.effective_workspaces_dir().join(&manifest.name);
|
||||
let workspace_dir = manifest.workspace.clone().unwrap_or_else(|| state_dir.clone());
|
||||
let workspace_dir = manifest
|
||||
.workspace
|
||||
.clone()
|
||||
.unwrap_or_else(|| state_dir.clone());
|
||||
if let Err(e) = ensure_state_dir(&state_dir, &workspace_dir) {
|
||||
warn!(agent_id = %agent_id, "Failed to backfill state_dir (streaming): {e}");
|
||||
}
|
||||
@@ -2699,7 +2702,10 @@ impl OpenFangKernel {
|
||||
// state_dir). See issue #1097.
|
||||
if manifest.state_dir.is_none() {
|
||||
let state_dir = self.config.effective_workspaces_dir().join(&manifest.name);
|
||||
let workspace_dir = manifest.workspace.clone().unwrap_or_else(|| state_dir.clone());
|
||||
let workspace_dir = manifest
|
||||
.workspace
|
||||
.clone()
|
||||
.unwrap_or_else(|| state_dir.clone());
|
||||
if let Err(e) = ensure_state_dir(&state_dir, &workspace_dir) {
|
||||
warn!(agent_id = %agent_id, "Failed to backfill state_dir: {e}");
|
||||
}
|
||||
@@ -5511,9 +5517,7 @@ impl OpenFangKernel {
|
||||
}
|
||||
for var in skill.manifest.config.values() {
|
||||
if let Some(env_name) = var.env.as_deref() {
|
||||
if let Some(providers) =
|
||||
env_to_provider.get(&env_name.to_ascii_uppercase())
|
||||
{
|
||||
if let Some(providers) = env_to_provider.get(&env_name.to_ascii_uppercase()) {
|
||||
for provider in providers {
|
||||
set.insert(provider.clone());
|
||||
}
|
||||
@@ -5528,9 +5532,7 @@ 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(providers) =
|
||||
env_to_provider.get(&env_name.to_ascii_uppercase())
|
||||
{
|
||||
if let Some(providers) = env_to_provider.get(&env_name.to_ascii_uppercase()) {
|
||||
for provider in providers {
|
||||
set.insert(provider.clone());
|
||||
}
|
||||
@@ -8022,8 +8024,7 @@ mod tests {
|
||||
|
||||
assert_eq!(merged.description, "new", "TOML edits must apply");
|
||||
assert_eq!(
|
||||
merged.workspace,
|
||||
entry.workspace,
|
||||
merged.workspace, entry.workspace,
|
||||
"kernel-assigned workspace must survive a TOML edit that omits it"
|
||||
);
|
||||
assert!(
|
||||
@@ -8438,9 +8439,7 @@ mod tests {
|
||||
let first_instance_id = instance.instance_id;
|
||||
|
||||
// Sanity: hand is Active and re-activation is rejected.
|
||||
assert!(kernel
|
||||
.activate_hand("lead", HashMap::new(), None)
|
||||
.is_err());
|
||||
assert!(kernel.activate_hand("lead", HashMap::new(), None).is_err());
|
||||
|
||||
// Simulate what POST /api/agents/{id}/stop now does for a hand-owned
|
||||
// agent: look up the instance and deactivate the hand (which also
|
||||
@@ -8880,7 +8879,8 @@ mod tests {
|
||||
"fallback timeout edits must be hot-reloadable"
|
||||
);
|
||||
assert!(
|
||||
plan.hot_actions.contains(&HotAction::ReloadFallbackProviders),
|
||||
plan.hot_actions
|
||||
.contains(&HotAction::ReloadFallbackProviders),
|
||||
"ReloadFallbackProviders must be present in the plan"
|
||||
);
|
||||
|
||||
@@ -9160,9 +9160,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_1188_referenced_providers_walks_mcp_env() {
|
||||
use openfang_types::config::{
|
||||
DefaultModelConfig, McpServerConfigEntry, McpTransportEntry,
|
||||
};
|
||||
use openfang_types::config::{DefaultModelConfig, McpServerConfigEntry, McpTransportEntry};
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let home_dir = tmp.path().join("openfang-1188-mcp");
|
||||
|
||||
@@ -69,8 +69,7 @@ fn env_timeout_secs(var: &str) -> Option<u64> {
|
||||
fn tool_timeout_for(tool_name: &str) -> Option<Duration> {
|
||||
let secs = match tool_name {
|
||||
"agent_send" | "agent_spawn" => {
|
||||
env_timeout_secs("OPENFANG_AGENT_TOOL_TIMEOUT_SECS")
|
||||
.unwrap_or(AGENT_TOOL_TIMEOUT_SECS)
|
||||
env_timeout_secs("OPENFANG_AGENT_TOOL_TIMEOUT_SECS").unwrap_or(AGENT_TOOL_TIMEOUT_SECS)
|
||||
}
|
||||
_ => env_timeout_secs("OPENFANG_TOOL_TIMEOUT_SECS").unwrap_or(TOOL_TIMEOUT_SECS),
|
||||
};
|
||||
@@ -3271,7 +3270,9 @@ mod tests {
|
||||
assert_eq!(blocks.len(), 2, "must preserve thinking + text");
|
||||
match &blocks[0] {
|
||||
ContentBlock::Thinking {
|
||||
thinking, signature, ..
|
||||
thinking,
|
||||
signature,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(thinking, "Let me reason carefully...");
|
||||
assert_eq!(signature.as_deref(), Some("sig_anthropic_xyz"));
|
||||
@@ -3367,7 +3368,10 @@ mod tests {
|
||||
let has_redacted = blocks
|
||||
.iter()
|
||||
.any(|b| matches!(b, ContentBlock::RedactedThinking { .. }));
|
||||
assert!(has_thinking, "Thinking block must be preserved on MaxTokens");
|
||||
assert!(
|
||||
has_thinking,
|
||||
"Thinking block must be preserved on MaxTokens"
|
||||
);
|
||||
assert!(
|
||||
has_redacted,
|
||||
"RedactedThinking block must be preserved on MaxTokens"
|
||||
@@ -3409,9 +3413,9 @@ mod tests {
|
||||
MessageContent::Blocks(b) => b,
|
||||
other => panic!("expected Blocks content for redacted-only turn, got {other:?}"),
|
||||
};
|
||||
let has_redacted = blocks
|
||||
.iter()
|
||||
.any(|b| matches!(b, ContentBlock::RedactedThinking { data } if data == "encrypted_only"));
|
||||
let has_redacted = blocks.iter().any(
|
||||
|b| matches!(b, ContentBlock::RedactedThinking { data } if data == "encrypted_only"),
|
||||
);
|
||||
assert!(
|
||||
has_redacted,
|
||||
"RedactedThinking-only turn must be preserved as Blocks"
|
||||
|
||||
@@ -189,7 +189,9 @@ enum ContentBlockAccum {
|
||||
/// Redacted (encrypted) thinking block streamed from Anthropic.
|
||||
/// The opaque `data` blob arrives on `content_block_start` and must be
|
||||
/// persisted so the next turn can echo it back verbatim.
|
||||
RedactedThinking { data: String },
|
||||
RedactedThinking {
|
||||
data: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -454,10 +456,8 @@ impl LlmDriver for AnthropicDriver {
|
||||
"thinking" => {
|
||||
// Some API versions ship the signature on
|
||||
// content_block_start instead of as a delta.
|
||||
let initial_sig = block["signature"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let initial_sig =
|
||||
block["signature"].as_str().unwrap_or("").to_string();
|
||||
blocks.push(ContentBlockAccum::Thinking {
|
||||
thinking: String::new(),
|
||||
signature: initial_sig,
|
||||
@@ -470,10 +470,7 @@ impl LlmDriver for AnthropicDriver {
|
||||
// verbatim so we can echo it back on the
|
||||
// next request — API rejects history
|
||||
// that strips redacted_thinking blocks.
|
||||
let data = block["data"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let data = block["data"].as_str().unwrap_or("").to_string();
|
||||
blocks.push(ContentBlockAccum::RedactedThinking { data });
|
||||
}
|
||||
_ => {}
|
||||
@@ -751,9 +748,7 @@ fn convert_message(msg: &Message) -> ApiMessage {
|
||||
if data.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ApiContentBlock::RedactedThinking {
|
||||
data: data.clone(),
|
||||
})
|
||||
Some(ApiContentBlock::RedactedThinking { data: data.clone() })
|
||||
}
|
||||
}
|
||||
ContentBlock::Unknown => None,
|
||||
|
||||
@@ -328,9 +328,7 @@ fn convert_content_block(block: &ContentBlock) -> Option<BedrockContentBlock> {
|
||||
}
|
||||
}
|
||||
// Image, Thinking, and Unknown are not supported — silently drop
|
||||
ContentBlock::Image { .. }
|
||||
| ContentBlock::Thinking { .. }
|
||||
| ContentBlock::Unknown => None,
|
||||
ContentBlock::Image { .. } | ContentBlock::Thinking { .. } | ContentBlock::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ use openfang_types::model_catalog::{
|
||||
HUGGINGFACE_BASE_URL, KIMI_CODING_BASE_URL, LEMONADE_BASE_URL, LMSTUDIO_BASE_URL,
|
||||
MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, NOVITA_BASE_URL, NVIDIA_NIM_BASE_URL,
|
||||
OLLAMA_BASE_URL, OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL,
|
||||
QWEN_BASE_URL, REPLICATE_BASE_URL, REQUESTY_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL,
|
||||
VLLM_BASE_URL, VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL,
|
||||
ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL,
|
||||
QWEN_BASE_URL, REPLICATE_BASE_URL, REQUESTY_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL,
|
||||
VENICE_BASE_URL, VLLM_BASE_URL, VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL,
|
||||
ZAI_BASE_URL, ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -460,8 +460,8 @@ impl LlmDriver for OpenAIDriver {
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
reasoning_content: None,
|
||||
reasoning: None,
|
||||
});
|
||||
reasoning: None,
|
||||
});
|
||||
}
|
||||
|
||||
// Convert messages
|
||||
@@ -474,8 +474,8 @@ impl LlmDriver for OpenAIDriver {
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
reasoning_content: None,
|
||||
reasoning: None,
|
||||
});
|
||||
reasoning: None,
|
||||
});
|
||||
}
|
||||
(Role::User, MessageContent::Text(text)) => {
|
||||
oai_messages.push(OaiMessage {
|
||||
@@ -484,8 +484,8 @@ impl LlmDriver for OpenAIDriver {
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
reasoning_content: None,
|
||||
reasoning: None,
|
||||
});
|
||||
reasoning: None,
|
||||
});
|
||||
}
|
||||
(Role::Assistant, MessageContent::Text(text)) => {
|
||||
oai_messages.push(OaiMessage {
|
||||
@@ -494,8 +494,8 @@ impl LlmDriver for OpenAIDriver {
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
reasoning_content: None,
|
||||
reasoning: None,
|
||||
});
|
||||
reasoning: None,
|
||||
});
|
||||
}
|
||||
(Role::User, MessageContent::Blocks(blocks)) => {
|
||||
// Handle tool results and images in user messages
|
||||
@@ -519,8 +519,8 @@ impl LlmDriver for OpenAIDriver {
|
||||
tool_calls: None,
|
||||
tool_call_id: Some(tool_use_id.clone()),
|
||||
reasoning_content: None,
|
||||
reasoning: None,
|
||||
});
|
||||
reasoning: None,
|
||||
});
|
||||
}
|
||||
ContentBlock::Text { text, .. } => {
|
||||
parts.push(OaiContentPart::Text { text: text.clone() });
|
||||
@@ -543,8 +543,8 @@ impl LlmDriver for OpenAIDriver {
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
reasoning_content: None,
|
||||
reasoning: None,
|
||||
});
|
||||
reasoning: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
(Role::Assistant, MessageContent::Blocks(blocks)) => {
|
||||
@@ -915,8 +915,8 @@ impl LlmDriver for OpenAIDriver {
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
reasoning_content: None,
|
||||
reasoning: None,
|
||||
});
|
||||
reasoning: None,
|
||||
});
|
||||
}
|
||||
|
||||
for msg in &request.messages {
|
||||
@@ -928,8 +928,8 @@ impl LlmDriver for OpenAIDriver {
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
reasoning_content: None,
|
||||
reasoning: None,
|
||||
});
|
||||
reasoning: None,
|
||||
});
|
||||
}
|
||||
(Role::User, MessageContent::Text(text)) => {
|
||||
oai_messages.push(OaiMessage {
|
||||
@@ -938,8 +938,8 @@ impl LlmDriver for OpenAIDriver {
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
reasoning_content: None,
|
||||
reasoning: None,
|
||||
});
|
||||
reasoning: None,
|
||||
});
|
||||
}
|
||||
(Role::Assistant, MessageContent::Text(text)) => {
|
||||
oai_messages.push(OaiMessage {
|
||||
@@ -948,8 +948,8 @@ impl LlmDriver for OpenAIDriver {
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
reasoning_content: None,
|
||||
reasoning: None,
|
||||
});
|
||||
reasoning: None,
|
||||
});
|
||||
}
|
||||
(Role::User, MessageContent::Blocks(blocks)) => {
|
||||
for block in blocks {
|
||||
@@ -969,8 +969,8 @@ impl LlmDriver for OpenAIDriver {
|
||||
tool_calls: None,
|
||||
tool_call_id: Some(tool_use_id.clone()),
|
||||
reasoning_content: None,
|
||||
reasoning: None,
|
||||
});
|
||||
reasoning: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1492,7 +1492,9 @@ impl LlmDriver for OpenAIDriver {
|
||||
// non-zero output_tokens so the agent loop doesn't misclassify
|
||||
// this as a "silent failure" and loop unnecessarily.
|
||||
if !content.is_empty() && usage.input_tokens == 0 && usage.output_tokens == 0 {
|
||||
debug!("Stream has content but no usage stats — setting synthetic output_tokens=1");
|
||||
debug!(
|
||||
"Stream has content but no usage stats — setting synthetic output_tokens=1"
|
||||
);
|
||||
usage.output_tokens = 1;
|
||||
}
|
||||
|
||||
@@ -1956,10 +1958,7 @@ mod tests {
|
||||
/// the upstream server is pre- or post-vLLM 0.19.
|
||||
#[test]
|
||||
fn test_assemble_emits_both_reasoning_fields_for_vllm_compat() {
|
||||
let driver = OpenAIDriver::new(
|
||||
"test".to_string(),
|
||||
"http://localhost:8000/v1".to_string(),
|
||||
);
|
||||
let driver = OpenAIDriver::new("test".to_string(), "http://localhost:8000/v1".to_string());
|
||||
let blocks = vec![
|
||||
ContentBlock::Thinking {
|
||||
thinking: "MARKER-vllm-019".to_string(),
|
||||
@@ -1994,8 +1993,7 @@ mod tests {
|
||||
/// change in #1157.
|
||||
#[test]
|
||||
fn test_assemble_no_reasoning_fields_for_plain_model() {
|
||||
let driver =
|
||||
OpenAIDriver::new("test".to_string(), "https://api.openai.com/v1".to_string());
|
||||
let driver = OpenAIDriver::new("test".to_string(), "https://api.openai.com/v1".to_string());
|
||||
let blocks = vec![ContentBlock::Text {
|
||||
text: "hi".to_string(),
|
||||
provider_metadata: None,
|
||||
@@ -2014,18 +2012,14 @@ mod tests {
|
||||
/// understand the new name.
|
||||
#[test]
|
||||
fn test_assemble_moonshot_keeps_legacy_field_only() {
|
||||
let driver = OpenAIDriver::new(
|
||||
"test".to_string(),
|
||||
"https://api.moonshot.cn/v1".to_string(),
|
||||
);
|
||||
let blocks = vec![
|
||||
ContentBlock::ToolUse {
|
||||
id: "call_1".to_string(),
|
||||
name: "search".to_string(),
|
||||
input: serde_json::json!({"q": "x"}),
|
||||
provider_metadata: None,
|
||||
},
|
||||
];
|
||||
let driver =
|
||||
OpenAIDriver::new("test".to_string(), "https://api.moonshot.cn/v1".to_string());
|
||||
let blocks = vec![ContentBlock::ToolUse {
|
||||
id: "call_1".to_string(),
|
||||
name: "search".to_string(),
|
||||
input: serde_json::json!({"q": "x"}),
|
||||
provider_metadata: None,
|
||||
}];
|
||||
let msg = assemble_assistant_message(&blocks, "kimi-k2", &driver);
|
||||
assert_eq!(
|
||||
msg.reasoning_content.as_deref(),
|
||||
@@ -2174,7 +2168,10 @@ mod tests {
|
||||
Some(OaiMessageContent::Text(t)) => t,
|
||||
_ => panic!("expected text content"),
|
||||
};
|
||||
assert_eq!(content, "answer", "visible content must not include <think>");
|
||||
assert_eq!(
|
||||
content, "answer",
|
||||
"visible content must not include <think>"
|
||||
);
|
||||
assert_eq!(
|
||||
msg.reasoning_content.as_deref(),
|
||||
Some("internal chain-of-thought"),
|
||||
@@ -2186,8 +2183,7 @@ mod tests {
|
||||
/// assistant message — preserve the legacy shape.
|
||||
#[test]
|
||||
fn test_assemble_assistant_no_thinking_is_plain() {
|
||||
let driver =
|
||||
OpenAIDriver::new("test".to_string(), "https://api.openai.com/v1".to_string());
|
||||
let driver = OpenAIDriver::new("test".to_string(), "https://api.openai.com/v1".to_string());
|
||||
let blocks = vec![ContentBlock::Text {
|
||||
text: "Hi.".to_string(),
|
||||
provider_metadata: None,
|
||||
|
||||
@@ -258,7 +258,6 @@ fn host_net_fetch(state: &GuestState, params: &serde_json::Value) -> serde_json:
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shell (capability-checked)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -561,7 +560,10 @@ mod tests {
|
||||
assert!(web_fetch::check_ssrf("http://127.0.0.1:8080/secret", &no_allow).is_err());
|
||||
assert!(web_fetch::check_ssrf("http://localhost:3000/api", &no_allow).is_err());
|
||||
assert!(web_fetch::check_ssrf("http://169.254.169.254/metadata", &no_allow).is_err());
|
||||
assert!(web_fetch::check_ssrf("http://metadata.google.internal/v1/instance", &no_allow).is_err());
|
||||
assert!(
|
||||
web_fetch::check_ssrf("http://metadata.google.internal/v1/instance", &no_allow)
|
||||
.is_err()
|
||||
);
|
||||
// These were previously missing from host_functions — now covered:
|
||||
assert!(web_fetch::check_ssrf("http://[::1]:8080/secret", &no_allow).is_err());
|
||||
assert!(web_fetch::check_ssrf("http://100.100.100.200/metadata", &no_allow).is_err());
|
||||
|
||||
@@ -10,8 +10,8 @@ use openfang_types::model_catalog::{
|
||||
HUGGINGFACE_BASE_URL, KIMI_CODING_BASE_URL, LEMONADE_BASE_URL, LMSTUDIO_BASE_URL,
|
||||
MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, NVIDIA_NIM_BASE_URL, OLLAMA_BASE_URL,
|
||||
OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL,
|
||||
REPLICATE_BASE_URL, REQUESTY_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL,
|
||||
VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL,
|
||||
REPLICATE_BASE_URL, REQUESTY_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL,
|
||||
VLLM_BASE_URL, VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL,
|
||||
ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
@@ -1052,10 +1052,7 @@ fn builtin_aliases() -> HashMap<String, String> {
|
||||
),
|
||||
("free", "openrouter/meta-llama/llama-3.3-70b-instruct:free"),
|
||||
("free-reasoning", "openrouter/deepseek/deepseek-r1:free"),
|
||||
(
|
||||
"openrouter/free-coder",
|
||||
"openrouter/qwen/qwen3-coder:free",
|
||||
),
|
||||
("openrouter/free-coder", "openrouter/qwen/qwen3-coder:free"),
|
||||
(
|
||||
"openrouter/free-large",
|
||||
"openrouter/openai/gpt-oss-120b:free",
|
||||
@@ -4715,9 +4712,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_openrouter_free_alias_supports_tools() {
|
||||
let catalog = ModelCatalog::new();
|
||||
let entry = catalog.find_model("openrouter/free").expect(
|
||||
"openrouter/free alias must resolve to a known model",
|
||||
);
|
||||
let entry = catalog
|
||||
.find_model("openrouter/free")
|
||||
.expect("openrouter/free alias must resolve to a known model");
|
||||
assert_eq!(entry.provider, "openrouter");
|
||||
assert!(
|
||||
entry.supports_tools,
|
||||
@@ -4730,9 +4727,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_openrouter_free_short_alias_supports_tools() {
|
||||
let catalog = ModelCatalog::new();
|
||||
let entry = catalog
|
||||
.find_model("free")
|
||||
.expect("free alias must resolve");
|
||||
let entry = catalog.find_model("free").expect("free alias must resolve");
|
||||
assert_eq!(entry.provider, "openrouter");
|
||||
assert!(
|
||||
entry.supports_tools,
|
||||
|
||||
@@ -3119,8 +3119,7 @@ async fn tool_image_generate(
|
||||
|
||||
// Closes #1051: route to a local OpenAI-compatible image generation
|
||||
// service when `media.image_gen_base_url` is set.
|
||||
let base_url_override = media_engine
|
||||
.and_then(|e| e.config().image_gen_base_url.as_deref());
|
||||
let base_url_override = media_engine.and_then(|e| e.config().image_gen_base_url.as_deref());
|
||||
let result = crate::image_gen::generate_image(&request, base_url_override).await?;
|
||||
|
||||
// Save images to workspace if available
|
||||
@@ -3636,9 +3635,9 @@ fn tool_skill_describe(
|
||||
.ok_or("Missing 'name' parameter")?
|
||||
.trim();
|
||||
let registry = skill_registry.ok_or("No skill registry available")?;
|
||||
let skill = registry
|
||||
.get(name)
|
||||
.ok_or_else(|| format!("Skill '{name}' not found. Use skill_list to see installed skills."))?;
|
||||
let skill = registry.get(name).ok_or_else(|| {
|
||||
format!("Skill '{name}' not found. Use skill_list to see installed skills.")
|
||||
})?;
|
||||
let body = skill
|
||||
.manifest
|
||||
.prompt_context
|
||||
@@ -3672,9 +3671,9 @@ async fn tool_skill_execute(
|
||||
.ok_or("Missing 'skill' parameter")?
|
||||
.trim();
|
||||
let registry = skill_registry.ok_or("No skill registry available")?;
|
||||
let skill = registry
|
||||
.get(skill_name)
|
||||
.ok_or_else(|| format!("Skill '{skill_name}' not found. Use skill_list to see installed skills."))?;
|
||||
let skill = registry.get(skill_name).ok_or_else(|| {
|
||||
format!("Skill '{skill_name}' not found. Use skill_list to see installed skills.")
|
||||
})?;
|
||||
|
||||
// If no tool name was given, default behavior depends on runtime.
|
||||
// For prompt-only skills, return the SKILL.md body (most useful response
|
||||
@@ -4017,14 +4016,14 @@ mod tests {
|
||||
async fn test_create_directory_creates_nested() {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let root = tmp.path();
|
||||
let result = tool_create_directory(
|
||||
&serde_json::json!({"path": "a/b/c"}),
|
||||
Some(root),
|
||||
)
|
||||
.await;
|
||||
let result = tool_create_directory(&serde_json::json!({"path": "a/b/c"}), Some(root)).await;
|
||||
assert!(result.is_ok(), "Expected Ok, got: {:?}", result);
|
||||
let expected = root.join("a").join("b").join("c");
|
||||
assert!(expected.is_dir(), "Expected directory to exist: {}", expected.display());
|
||||
assert!(
|
||||
expected.is_dir(),
|
||||
"Expected directory to exist: {}",
|
||||
expected.display()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4032,18 +4031,10 @@ mod tests {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let root = tmp.path();
|
||||
// First create
|
||||
let r1 = tool_create_directory(
|
||||
&serde_json::json!({"path": "data/logs"}),
|
||||
Some(root),
|
||||
)
|
||||
.await;
|
||||
let r1 = tool_create_directory(&serde_json::json!({"path": "data/logs"}), Some(root)).await;
|
||||
assert!(r1.is_ok());
|
||||
// Second create on existing dir should also succeed
|
||||
let r2 = tool_create_directory(
|
||||
&serde_json::json!({"path": "data/logs"}),
|
||||
Some(root),
|
||||
)
|
||||
.await;
|
||||
let r2 = tool_create_directory(&serde_json::json!({"path": "data/logs"}), Some(root)).await;
|
||||
assert!(r2.is_ok(), "Expected idempotent success, got: {:?}", r2);
|
||||
}
|
||||
|
||||
@@ -4063,23 +4054,27 @@ mod tests {
|
||||
"test-id",
|
||||
"create_directory",
|
||||
&serde_json::json!({"path": "nested/folder"}),
|
||||
None, // kernel
|
||||
None, // allowed_tools
|
||||
None, // caller_agent_id
|
||||
None, // skill_registry
|
||||
None, // mcp_connections
|
||||
None, // web_ctx
|
||||
None, // browser_ctx
|
||||
None, // allowed_env_vars
|
||||
None, // kernel
|
||||
None, // allowed_tools
|
||||
None, // caller_agent_id
|
||||
None, // skill_registry
|
||||
None, // mcp_connections
|
||||
None, // web_ctx
|
||||
None, // browser_ctx
|
||||
None, // allowed_env_vars
|
||||
Some(root.as_path()), // workspace_root
|
||||
None, // media_engine
|
||||
None, // exec_policy
|
||||
None, // tts_engine
|
||||
None, // docker_config
|
||||
None, // process_manager
|
||||
None, // media_engine
|
||||
None, // exec_policy
|
||||
None, // tts_engine
|
||||
None, // docker_config
|
||||
None, // process_manager
|
||||
)
|
||||
.await;
|
||||
assert!(!result.is_error, "Expected success, got: {}", result.content);
|
||||
assert!(
|
||||
!result.is_error,
|
||||
"Expected success, got: {}",
|
||||
result.content
|
||||
);
|
||||
assert!(root.join("nested").join("folder").is_dir());
|
||||
}
|
||||
|
||||
|
||||
@@ -60,11 +60,8 @@ impl InstallOptions {
|
||||
|
||||
/// Well-known filenames the installer searches for a detached signature
|
||||
/// envelope, in priority order.
|
||||
const SIGNATURE_CANDIDATES: &[&str] = &[
|
||||
"signature.json",
|
||||
"skill.toml.sig.json",
|
||||
"SKILL.md.sig.json",
|
||||
];
|
||||
const SIGNATURE_CANDIDATES: &[&str] =
|
||||
&["signature.json", "skill.toml.sig.json", "SKILL.md.sig.json"];
|
||||
|
||||
/// Normalize a manifest text for content-binding comparison.
|
||||
///
|
||||
@@ -167,10 +164,7 @@ pub fn load_signature(skill_dir: &Path) -> Result<Option<SignedManifest>, SkillE
|
||||
/// Returns `SkillError::SecurityBlocked` when enforcement is on and the
|
||||
/// skill fails any of those checks. On failure the caller is expected to
|
||||
/// remove `skill_dir` to keep the skills directory clean.
|
||||
pub fn enforce_require_signed(
|
||||
skill_dir: &Path,
|
||||
opts: &InstallOptions,
|
||||
) -> Result<(), SkillError> {
|
||||
pub fn enforce_require_signed(skill_dir: &Path, opts: &InstallOptions) -> Result<(), SkillError> {
|
||||
if !opts.require_signed {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -214,8 +208,7 @@ pub fn enforce_require_signed(
|
||||
// CRLF→LF normalization (applied symmetrically to both sides).
|
||||
// `package.json` is included because openclaw_compat treats it as a
|
||||
// valid SKILL manifest source.
|
||||
const MANIFEST_CANDIDATES: &[&str] =
|
||||
&["skill.toml", "SKILL.md", "skill.md", "package.json"];
|
||||
const MANIFEST_CANDIDATES: &[&str] = &["skill.toml", "SKILL.md", "skill.md", "package.json"];
|
||||
let normalized_envelope = normalize_manifest_text(&envelope.manifest);
|
||||
let mut bound = false;
|
||||
for name in MANIFEST_CANDIDATES {
|
||||
|
||||
@@ -434,7 +434,9 @@ mod tests {
|
||||
let restored: ContentBlock = serde_json::from_str(&serialized).unwrap();
|
||||
match restored {
|
||||
ContentBlock::Thinking {
|
||||
thinking, signature, ..
|
||||
thinking,
|
||||
signature,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(thinking, "Let me reason about this carefully...");
|
||||
assert_eq!(
|
||||
@@ -517,7 +519,9 @@ mod tests {
|
||||
assert_eq!(blocks.len(), 2);
|
||||
match &blocks[0] {
|
||||
ContentBlock::Thinking {
|
||||
thinking, signature, ..
|
||||
thinking,
|
||||
signature,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(thinking, "Internal reasoning");
|
||||
assert_eq!(signature.as_deref(), Some("sig_xyz"));
|
||||
@@ -547,8 +551,7 @@ mod tests {
|
||||
fn test_provider_msg_id_is_recorded_but_not_primary() {
|
||||
// The LLM-supplied identifier is preserved for debugging only — the
|
||||
// server-generated `msg_id` is unchanged and remains the primary key.
|
||||
let msg =
|
||||
Message::assistant("hi").with_provider_msg_id("msg_abc123_anthropic_collidable");
|
||||
let msg = Message::assistant("hi").with_provider_msg_id("msg_abc123_anthropic_collidable");
|
||||
assert_eq!(
|
||||
msg.provider_msg_id.as_deref(),
|
||||
Some("msg_abc123_anthropic_collidable")
|
||||
|
||||
Reference in New Issue
Block a user