pub(crate) mod capability;
pub(crate) mod client;
pub(crate) mod hooks;
pub(crate) mod manage;
pub(crate) mod manager;
pub(crate) mod package;
pub(crate) mod protocol;
pub(crate) mod store;
pub(crate) use capability::ExtensionCapability;
pub(crate) use manage::ExtensionsCapability;
pub(crate) use package::{discover_extensions, extension_capability_id, extensions_dir};
#[cfg(test)]
mod spawn_tests {
use super::capability::ExtensionCapability;
use super::package::{ExtensionPackage, parse_manifest};
use everruns_core::capabilities::Capability;
use everruns_core::tools::ToolExecutionResult;
use serde_json::json;
fn python3() -> Option<String> {
which_python(&["python3", "python"])
}
fn have_cargo() -> bool {
std::process::Command::new("cargo")
.arg("--version")
.output()
.is_ok_and(|out| out.status.success())
}
fn sdk_example_package() -> ExtensionPackage {
let manifest_path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("crates/yolop-yep/Cargo.toml");
let manifest = parse_manifest(
&json!({
"name": "echo",
"description": "yolop-yep SDK example.",
"yolop": {
"protocol_version": "1.0",
"capabilityServer": {
"command": "cargo",
"args": ["run", "-q", "--example", "echo",
"--manifest-path", manifest_path.display().to_string()]
},
"tools": [
{ "name": "echo", "description": "Echo text.", "never_defer": true }
],
"prompt": true,
"dynamic_prompt": true,
"hooks": [
{ "event": "pre_tool_use", "tool_name_glob": "*" }
]
}
})
.to_string(),
)
.expect("sdk example manifest");
ExtensionPackage {
dir: std::env::temp_dir(),
manifest,
}
}
#[tokio::test]
async fn yolop_yep_sdk_example_server_interops_with_the_host() {
if !have_cargo() {
eprintln!("skipping: cargo not available");
return;
}
let capability = ExtensionCapability::new(sdk_example_package(), std::env::temp_dir());
let tools = capability.tools();
let echo = tools.iter().find(|t| t.name() == "echo").expect("echo");
match echo.execute(json!({ "text": "via-sdk" })).await {
ToolExecutionResult::Success(v) => assert_eq!(v["echoed"], "via-sdk"),
other => panic!("expected success, got {other:?}"),
}
let ctx = everruns_core::capabilities::SystemPromptContext::without_file_store(
everruns_core::typed_id::SessionId::new(),
);
let prompt = capability
.system_prompt_contribution(&ctx)
.await
.expect("prompt");
assert!(prompt.contains("dynamic echo prompt"), "{prompt}");
use everruns_core::atoms::PreToolUseDecision;
use everruns_core::tool_types::{BuiltinTool, ToolCall, ToolDefinition};
let hooks = capability.pre_tool_use_hooks_with_config(&json!(null));
let tool_def = ToolDefinition::Builtin(BuiltinTool {
name: "bash".into(),
display_name: None,
description: "run".into(),
parameters: json!({ "type": "object" }),
policy: Default::default(),
category: None,
deferrable: Default::default(),
hints: Default::default(),
full_parameters: None,
});
let ctx2 =
everruns_core::traits::ToolContext::new(everruns_core::typed_id::SessionId::new());
let deny = ToolCall {
id: "1".into(),
name: "bash".into(),
arguments: json!({ "forbidden": true }),
};
assert!(matches!(
hooks[0].before_exec(deny, &tool_def, &ctx2).await,
PreToolUseDecision::Block { .. }
));
}
fn which_python(candidates: &[&str]) -> Option<String> {
for candidate in candidates {
if std::process::Command::new(candidate)
.arg("--version")
.output()
.is_ok_and(|out| out.status.success())
{
return Some(candidate.to_string());
}
}
None
}
fn fixture_package(python: &str) -> ExtensionPackage {
let server = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/yep_echo_server.py");
let manifest = parse_manifest(
&json!({
"name": "echo",
"description": "Echo fixture.",
"yolop": {
"protocol_version": "1.0",
"capabilityServer": {
"command": python,
"args": [server.display().to_string()]
},
"tools": [
{ "name": "echo", "description": "Echo text.",
"schema": { "type": "object" }, "never_defer": true },
{ "name": "unserved", "description": "Never served." }
],
"prompt": true
}
})
.to_string(),
)
.expect("fixture manifest");
ExtensionPackage {
dir: std::env::temp_dir(),
manifest,
}
}
#[tokio::test]
async fn spawns_real_server_handshakes_and_calls_tool() {
let Some(python) = python3() else {
eprintln!("skipping: python3 not available");
return;
};
let capability = ExtensionCapability::new(fixture_package(&python), std::env::temp_dir());
assert_eq!(capability.id(), "ext:echo");
assert_eq!(capability.never_defer_tools(), vec!["echo".to_string()]);
let tools = capability.tools();
let echo = tools
.iter()
.find(|tool| tool.name() == "echo")
.expect("echo tool");
match echo.execute(json!({"text": "round-trip"})).await {
ToolExecutionResult::Success(value) => {
assert_eq!(value["echoed"], "round-trip");
}
other => panic!("expected success, got {other:?}"),
}
let unserved = tools
.iter()
.find(|tool| tool.name() == "unserved")
.expect("unserved tool");
match unserved.execute(json!({})).await {
ToolExecutionResult::ToolError(message) => {
assert!(message.contains("not served"), "{message}");
}
other => panic!("expected tool error, got {other:?}"),
}
}
#[tokio::test]
async fn prompt_contribution_comes_from_handshake_clamped_by_manifest() {
let Some(python) = python3() else {
eprintln!("skipping: python3 not available");
return;
};
let capability = ExtensionCapability::new(fixture_package(&python), std::env::temp_dir());
let ctx = everruns_core::capabilities::SystemPromptContext::without_file_store(
everruns_core::typed_id::SessionId::new(),
);
let contribution = capability
.system_prompt_contribution(&ctx)
.await
.expect("prompt facet");
assert!(contribution.contains("<capability id=\"ext:echo\">"));
assert!(contribution.contains("echo fixture prompt"));
}
fn hooks_package(python: &str) -> ExtensionPackage {
let server = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/yep_echo_server.py");
let manifest = parse_manifest(
&json!({
"name": "echo",
"description": "Echo fixture.",
"yolop": {
"protocol_version": "1.0",
"capabilityServer": { "command": python,
"args": [server.display().to_string()] },
"dynamic_prompt": true,
"hooks": [
{ "event": "pre_tool_use", "tool_name_glob": "*",
"timeout_ms": 5000, "on_error": "warn" }
]
}
})
.to_string(),
)
.expect("hooks manifest");
ExtensionPackage {
dir: std::env::temp_dir(),
manifest,
}
}
#[tokio::test]
async fn pre_tool_use_hook_blocks_via_server_decision() {
use everruns_core::atoms::PreToolUseDecision;
use everruns_core::tool_types::{BuiltinTool, ToolCall, ToolDefinition};
let Some(python) = python3() else {
eprintln!("skipping: python3 not available");
return;
};
let capability = ExtensionCapability::new(hooks_package(&python), std::env::temp_dir());
let hooks = capability.pre_tool_use_hooks_with_config(&json!(null));
assert_eq!(hooks.len(), 1);
let hook = &hooks[0];
let tool_def = ToolDefinition::Builtin(BuiltinTool {
name: "bash".into(),
display_name: None,
description: "run".into(),
parameters: json!({ "type": "object" }),
policy: Default::default(),
category: None,
deferrable: Default::default(),
hints: Default::default(),
full_parameters: None,
});
let ctx =
everruns_core::traits::ToolContext::new(everruns_core::typed_id::SessionId::new());
let allow = ToolCall {
id: "1".into(),
name: "bash".into(),
arguments: json!({"command": "ls"}),
};
assert!(matches!(
hook.before_exec(allow, &tool_def, &ctx).await,
PreToolUseDecision::Continue(_)
));
let deny = ToolCall {
id: "2".into(),
name: "bash".into(),
arguments: json!({"forbidden": true}),
};
match hook.before_exec(deny, &tool_def, &ctx).await {
PreToolUseDecision::Block { reason, .. } => {
assert!(reason.contains("forbidden"), "{reason}");
}
other => panic!("expected block, got {other:?}"),
}
}
#[tokio::test]
async fn dynamic_prompt_is_served_per_turn() {
let Some(python) = python3() else {
eprintln!("skipping: python3 not available");
return;
};
let capability = ExtensionCapability::new(hooks_package(&python), std::env::temp_dir());
let ctx = everruns_core::capabilities::SystemPromptContext::without_file_store(
everruns_core::typed_id::SessionId::new(),
);
let contribution = capability
.system_prompt_contribution(&ctx)
.await
.expect("dynamic prompt");
assert!(
contribution.contains("dynamic echo prompt"),
"{contribution}"
);
}
}