feat(mcp): 将单次调用审阅绑定到已验证实例
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
//! Host-memory call reviews. The UI/registry must establish actual user consent
|
||||
//! before confirm; no renderer command or automatic-consent policy is added here.
|
||||
use crate::{
|
||||
extension_mcp_tools::{Description, Tool},
|
||||
extension_permit::{Claims, ExecutionKind},
|
||||
workspace::{HostError, Result},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct Identity {
|
||||
instance_id: String,
|
||||
kind: ExecutionKind,
|
||||
source: String,
|
||||
namespace: String,
|
||||
package_id: String,
|
||||
version: String,
|
||||
vault_id: String,
|
||||
permissions: BTreeSet<String>,
|
||||
execution_digest: String,
|
||||
}
|
||||
impl Identity {
|
||||
/// Only called after launch permit/entry/context validation.
|
||||
pub(crate) fn from_claims(claims: &Claims) -> Result<Self> {
|
||||
let bytes = Zeroizing::new(
|
||||
serde_json::to_vec(claims)
|
||||
.map_err(|_| HostError::new("EXTENSION_CALL_BINDING_INVALID"))?,
|
||||
);
|
||||
let mut hash = Sha256::new();
|
||||
hash.update(b"OpenNexus execution identity v1\0");
|
||||
hash.update(bytes.as_slice());
|
||||
Ok(Self {
|
||||
instance_id: uuid::Uuid::new_v4().to_string(),
|
||||
kind: claims.kind,
|
||||
source: claims.source.clone(),
|
||||
namespace: claims.namespace.clone(),
|
||||
package_id: claims.package_id.clone(),
|
||||
version: claims.version.clone(),
|
||||
vault_id: claims.vault_id.clone(),
|
||||
permissions: claims.permissions.clone(),
|
||||
execution_digest: format!("{:x}", hash.finalize()),
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
pub struct Review {
|
||||
pub review_id: String,
|
||||
pub identity: Identity,
|
||||
pub tool: Description,
|
||||
pub arguments: Value,
|
||||
pub contract_digest: String,
|
||||
pub valid_for_seconds: u64,
|
||||
}
|
||||
struct Pending {
|
||||
name: String,
|
||||
arguments: Value,
|
||||
contract_digest: String,
|
||||
expires: Instant,
|
||||
bytes: usize,
|
||||
}
|
||||
/// An in-process, non-cloneable, non-serializable, single-consumption capability.
|
||||
/// Tool name and arguments cannot be replaced after review confirmation.
|
||||
pub struct ApprovedCall {
|
||||
instance: String,
|
||||
epoch: String,
|
||||
call: Pending,
|
||||
}
|
||||
pub(crate) struct Invocation {
|
||||
pub name: String,
|
||||
pub arguments: Value,
|
||||
pub contract_digest: String,
|
||||
}
|
||||
pub(crate) struct Gate {
|
||||
identity: Identity,
|
||||
epoch: String,
|
||||
pending: BTreeMap<String, Pending>,
|
||||
}
|
||||
impl Gate {
|
||||
pub(crate) fn new(identity: Identity) -> Self {
|
||||
Self {
|
||||
identity,
|
||||
epoch: uuid::Uuid::new_v4().to_string(),
|
||||
pending: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
pub(crate) fn invalidate(&mut self) {
|
||||
self.epoch = uuid::Uuid::new_v4().to_string();
|
||||
self.pending.clear();
|
||||
}
|
||||
pub(crate) fn review(&mut self, tool: &Tool, arguments: Value) -> Result<Review> {
|
||||
tool.validate_arguments(&arguments)?;
|
||||
self.pending
|
||||
.retain(|_, pending| pending.expires > Instant::now());
|
||||
let bytes = serde_json::to_vec(&arguments)
|
||||
.map_err(|_| HostError::new("EXTENSION_CALL_REVIEW_INVALID"))?
|
||||
.len();
|
||||
if self.pending.len() >= 64
|
||||
|| self.pending.values().map(|p| p.bytes).sum::<usize>() + bytes > 2 * 1024 * 1024
|
||||
{
|
||||
return Err(HostError::new("EXTENSION_CALL_REVIEW_LIMIT"));
|
||||
}
|
||||
let tool = tool.description();
|
||||
let contract_digest = contract_digest(&tool)?;
|
||||
let review_id = uuid::Uuid::new_v4().to_string();
|
||||
self.pending.insert(
|
||||
review_id.clone(),
|
||||
Pending {
|
||||
name: tool.name.clone(),
|
||||
arguments: arguments.clone(),
|
||||
contract_digest: contract_digest.clone(),
|
||||
expires: Instant::now() + Duration::from_secs(120),
|
||||
bytes,
|
||||
},
|
||||
);
|
||||
Ok(Review {
|
||||
review_id,
|
||||
identity: self.identity.clone(),
|
||||
tool,
|
||||
arguments,
|
||||
contract_digest,
|
||||
valid_for_seconds: 120,
|
||||
})
|
||||
}
|
||||
/// The authenticated Host approval route must verify user consent first.
|
||||
pub(crate) fn confirm(&mut self, review_id: &str) -> Result<ApprovedCall> {
|
||||
let call = self
|
||||
.pending
|
||||
.remove(review_id)
|
||||
.ok_or_else(|| HostError::new("EXTENSION_CALL_REVIEW_UNKNOWN"))?;
|
||||
if call.expires <= Instant::now() {
|
||||
return Err(HostError::new("EXTENSION_CALL_REVIEW_EXPIRED"));
|
||||
}
|
||||
Ok(ApprovedCall {
|
||||
instance: self.identity.instance_id.clone(),
|
||||
epoch: self.epoch.clone(),
|
||||
call,
|
||||
})
|
||||
}
|
||||
pub(crate) fn consume(&self, approved: ApprovedCall) -> Result<Invocation> {
|
||||
if approved.instance != self.identity.instance_id {
|
||||
return Err(HostError::new("EXTENSION_CALL_INSTANCE_MISMATCH"));
|
||||
}
|
||||
if approved.epoch != self.epoch {
|
||||
return Err(HostError::new("EXTENSION_CALL_CATALOG_CHANGED"));
|
||||
}
|
||||
if approved.call.expires <= Instant::now() {
|
||||
return Err(HostError::new("EXTENSION_CALL_REVIEW_EXPIRED"));
|
||||
}
|
||||
Ok(Invocation {
|
||||
name: approved.call.name,
|
||||
arguments: approved.call.arguments,
|
||||
contract_digest: approved.call.contract_digest,
|
||||
})
|
||||
}
|
||||
}
|
||||
pub(crate) fn contract_digest(tool: &Description) -> Result<String> {
|
||||
let mut hash = Sha256::new();
|
||||
hash.update(b"OpenNexus MCP tool contract v1\0");
|
||||
hash.update(
|
||||
serde_json::to_vec(tool).map_err(|_| HostError::new("EXTENSION_CALL_REVIEW_INVALID"))?,
|
||||
);
|
||||
Ok(format!("{:x}", hash.finalize()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::extension_mcp_tools::Catalog;
|
||||
use serde_json::json;
|
||||
fn identity() -> Identity {
|
||||
Identity {
|
||||
instance_id: uuid::Uuid::new_v4().to_string(),
|
||||
kind: ExecutionKind::Mcp,
|
||||
source: "https://catalog.example/".into(),
|
||||
namespace: "examples".into(),
|
||||
package_id: "echo".into(),
|
||||
version: "1.0.0".into(),
|
||||
vault_id: uuid::Uuid::new_v4().to_string(),
|
||||
permissions: BTreeSet::new(),
|
||||
execution_digest: "a".repeat(64),
|
||||
}
|
||||
}
|
||||
fn tool() -> std::sync::Arc<Tool> {
|
||||
Catalog::discover(|_| {
|
||||
Ok(json!({"tools":[{"name":"echo","inputSchema":{"type":"object"}}]}))
|
||||
})
|
||||
.unwrap()
|
||||
.tool("echo")
|
||||
.unwrap()
|
||||
}
|
||||
#[test]
|
||||
fn review_freezes_arguments_and_is_single_use_instance_epoch_and_expiry_bound() {
|
||||
let mut gate = Gate::new(identity());
|
||||
let tool = tool();
|
||||
let mut review = gate.review(&tool, json!({"value":"original"})).unwrap();
|
||||
review.arguments = json!({"value":"tampered"});
|
||||
review.tool.name = "tampered".into();
|
||||
let approved = gate.confirm(&review.review_id).unwrap();
|
||||
assert_eq!(
|
||||
gate.confirm(&review.review_id).err().unwrap().code,
|
||||
"EXTENSION_CALL_REVIEW_UNKNOWN"
|
||||
);
|
||||
let call = gate.consume(approved).unwrap();
|
||||
assert_eq!(call.name, "echo");
|
||||
assert_eq!(call.arguments, json!({"value":"original"}));
|
||||
assert_eq!(
|
||||
call.contract_digest,
|
||||
contract_digest(&tool.description()).unwrap()
|
||||
);
|
||||
let review = gate.review(&tool, json!({})).unwrap();
|
||||
let approved = gate.confirm(&review.review_id).unwrap();
|
||||
assert_eq!(
|
||||
Gate::new(identity()).consume(approved).err().unwrap().code,
|
||||
"EXTENSION_CALL_INSTANCE_MISMATCH"
|
||||
);
|
||||
let review = gate.review(&tool, json!({})).unwrap();
|
||||
let approved = gate.confirm(&review.review_id).unwrap();
|
||||
let pending = gate.review(&tool, json!({})).unwrap();
|
||||
gate.invalidate();
|
||||
assert_eq!(
|
||||
gate.consume(approved).err().unwrap().code,
|
||||
"EXTENSION_CALL_CATALOG_CHANGED"
|
||||
);
|
||||
assert_eq!(
|
||||
gate.confirm(&pending.review_id).err().unwrap().code,
|
||||
"EXTENSION_CALL_REVIEW_UNKNOWN"
|
||||
);
|
||||
let review = gate.review(&tool, json!({})).unwrap();
|
||||
gate.pending.get_mut(&review.review_id).unwrap().expires = Instant::now();
|
||||
assert_eq!(
|
||||
gate.confirm(&review.review_id).err().unwrap().code,
|
||||
"EXTENSION_CALL_REVIEW_EXPIRED"
|
||||
);
|
||||
let review = gate.review(&tool, json!({})).unwrap();
|
||||
let mut approved = gate.confirm(&review.review_id).unwrap();
|
||||
approved.call.expires = Instant::now();
|
||||
assert_eq!(
|
||||
gate.consume(approved).err().unwrap().code,
|
||||
"EXTENSION_CALL_REVIEW_EXPIRED"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn pending_review_count_and_bytes_are_bounded_and_invalidated_slots_are_reusable() {
|
||||
let tool = tool();
|
||||
let mut gate = Gate::new(identity());
|
||||
for _ in 0..64 {
|
||||
gate.review(&tool, json!({})).unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
gate.review(&tool, json!({})).err().unwrap().code,
|
||||
"EXTENSION_CALL_REVIEW_LIMIT"
|
||||
);
|
||||
gate.invalidate();
|
||||
for _ in 0..10 {
|
||||
gate.review(&tool, json!({"value":"x".repeat(200_000)}))
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
gate.review(&tool, json!({"value":"x".repeat(200_000)}))
|
||||
.err()
|
||||
.unwrap()
|
||||
.code,
|
||||
"EXTENSION_CALL_REVIEW_LIMIT"
|
||||
);
|
||||
for value in gate.pending.values_mut() {
|
||||
value.expires = Instant::now();
|
||||
}
|
||||
gate.review(&tool, json!({"value":"x".repeat(200_000)}))
|
||||
.unwrap();
|
||||
assert_eq!(gate.pending.len(), 1);
|
||||
}
|
||||
}
|
||||
@@ -874,6 +874,7 @@ mod tests {
|
||||
"mcp_pages",
|
||||
"mcp_bad_result",
|
||||
"mcp_idle_change",
|
||||
"mcp_review_lock",
|
||||
];
|
||||
if _mcp_deadline {
|
||||
mcp_modes.push("mcp_deadline");
|
||||
@@ -898,7 +899,7 @@ mod tests {
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
assert_eq!(
|
||||
session
|
||||
.call_tool("echo", serde_json::json!({}), &cancel)
|
||||
.test_call_tool("echo", serde_json::json!({}), &cancel)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"EXTENSION_MCP_NOT_INITIALIZED"
|
||||
@@ -915,7 +916,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
session
|
||||
.call_tool("echo", serde_json::json!({}), &cancel)
|
||||
.test_call_tool("echo", serde_json::json!({}), &cancel)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"EXTENSION_MCP_CATALOG_REQUIRED"
|
||||
@@ -923,14 +924,14 @@ mod tests {
|
||||
assert_eq!(session.refresh_tools(&cancel).unwrap()[0].name, "echo");
|
||||
assert_eq!(
|
||||
session
|
||||
.call_tool("missing", serde_json::json!({}), &cancel)
|
||||
.test_call_tool("missing", serde_json::json!({}), &cancel)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"EXTENSION_MCP_TOOL_NOT_FOUND"
|
||||
);
|
||||
assert_eq!(
|
||||
session
|
||||
.call_tool("echo", serde_json::json!({"unexpected":1}), &cancel)
|
||||
.test_call_tool("echo", serde_json::json!({"unexpected":1}), &cancel)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"EXTENSION_MCP_ARGUMENTS_INVALID"
|
||||
@@ -944,9 +945,42 @@ mod tests {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut earlier = None;
|
||||
if mode == "mcp" {
|
||||
let review = session.review_call("echo", serde_json::json!({})).unwrap();
|
||||
let identity = serde_json::to_value(&review.identity).unwrap();
|
||||
assert_eq!(identity["package_id"], mcp.package_id);
|
||||
assert_eq!(identity["vault_id"], mcp.vault_id);
|
||||
assert_eq!(identity["source"], mcp.source);
|
||||
assert_eq!(identity["execution_digest"].as_str().unwrap().len(), 64);
|
||||
earlier = Some(session.confirm_call(&review.review_id).unwrap());
|
||||
}
|
||||
let tool_started = std::time::Instant::now();
|
||||
let result = session.call_tool("echo", serde_json::json!({}), &cancel);
|
||||
let result = if mode == "mcp" {
|
||||
let mut review =
|
||||
session.review_call("echo", serde_json::json!({})).unwrap();
|
||||
review.arguments = serde_json::json!({"unexpected":1});
|
||||
let approved = session.confirm_call(&review.review_id).unwrap();
|
||||
session.call_tool(approved, &cancel)
|
||||
} else if mode == "mcp_review_lock" {
|
||||
let review = session.review_call("echo", serde_json::json!({})).unwrap();
|
||||
let approved = session.confirm_call(&review.review_id).unwrap();
|
||||
broker.lock();
|
||||
session.call_tool(approved, &cancel)
|
||||
} else {
|
||||
session.test_call_tool("echo", serde_json::json!({}), &cancel)
|
||||
};
|
||||
match mode {
|
||||
"mcp_review_lock" => {
|
||||
assert_eq!(result.unwrap_err().code, "CREDENTIALS_LOCKED");
|
||||
broker
|
||||
.unlock(Zeroizing::new(b"native fixture passphrase".to_vec()))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
session.refresh_tools(&cancel).err().unwrap().code,
|
||||
"EXTENSION_MCP_SESSION_FAILED"
|
||||
);
|
||||
}
|
||||
"mcp_bad_result" => assert_eq!(
|
||||
result.unwrap_err().code,
|
||||
"EXTENSION_MCP_TOOL_RESULT_INVALID"
|
||||
@@ -956,7 +990,7 @@ mod tests {
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
assert_eq!(
|
||||
session
|
||||
.call_tool("echo", serde_json::json!({}), &cancel)
|
||||
.test_call_tool("echo", serde_json::json!({}), &cancel)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"EXTENSION_MCP_CATALOG_REQUIRED"
|
||||
@@ -1005,7 +1039,7 @@ mod tests {
|
||||
assert_eq!(result.unwrap_err().code, "EXTENSION_MCP_REMOTE_ERROR");
|
||||
assert_eq!(
|
||||
session
|
||||
.call_tool("echo", serde_json::json!({}), &cancel)
|
||||
.test_call_tool("echo", serde_json::json!({}), &cancel)
|
||||
.unwrap()["content"][0]["text"],
|
||||
"native MCP success"
|
||||
);
|
||||
@@ -1014,9 +1048,15 @@ mod tests {
|
||||
assert_eq!(result.unwrap()["content"][0]["text"], "native MCP success");
|
||||
assert!(session.take_tools_changed());
|
||||
assert!(!session.take_tools_changed());
|
||||
if let Some(approved) = earlier {
|
||||
assert_eq!(
|
||||
session.call_tool(approved, &cancel).unwrap_err().code,
|
||||
"EXTENSION_CALL_CATALOG_CHANGED"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
session
|
||||
.call_tool("echo", serde_json::json!({}), &cancel)
|
||||
.test_call_tool("echo", serde_json::json!({}), &cancel)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"EXTENSION_MCP_CATALOG_REQUIRED"
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct Context<'a> {
|
||||
pub struct PreparedLaunch {
|
||||
data: LaunchData,
|
||||
lease: crate::extension_permit::Lease,
|
||||
identity: crate::extension_call_authorization::Identity,
|
||||
path: std::path::PathBuf,
|
||||
entry: String,
|
||||
tree: String,
|
||||
@@ -29,6 +30,7 @@ pub struct PreparedLaunch {
|
||||
pub struct LeasedSuspended<'a> {
|
||||
process: crate::extension_process::Suspended<'a>,
|
||||
lease: crate::extension_permit::Lease,
|
||||
identity: crate::extension_call_authorization::Identity,
|
||||
}
|
||||
impl PreparedLaunch {
|
||||
pub fn create_suspended<'a>(
|
||||
@@ -48,6 +50,7 @@ impl PreparedLaunch {
|
||||
Ok(LeasedSuspended {
|
||||
process,
|
||||
lease: self.lease,
|
||||
identity: self.identity,
|
||||
})
|
||||
}
|
||||
pub fn create_suspended_with_stdio<'a>(
|
||||
@@ -70,6 +73,7 @@ impl PreparedLaunch {
|
||||
LeasedSuspended {
|
||||
process,
|
||||
lease: self.lease,
|
||||
identity: self.identity,
|
||||
},
|
||||
io,
|
||||
))
|
||||
@@ -80,7 +84,7 @@ impl<'a> LeasedSuspended<'a> {
|
||||
/// Live trust, active installation, broker and all sandbox resource policy
|
||||
/// requirements must also hold. A lease does not establish those conditions.
|
||||
pub unsafe fn resume(self) -> Result<crate::extension_process::Running<'a>> {
|
||||
unsafe { self.process.resume_with_lease(self.lease) }
|
||||
unsafe { self.process.resume_with_lease(self.lease, self.identity) }
|
||||
}
|
||||
}
|
||||
struct EnvironmentValues(BTreeMap<String, String>);
|
||||
@@ -153,6 +157,7 @@ impl Context<'_> {
|
||||
Ok(PreparedLaunch {
|
||||
data,
|
||||
lease,
|
||||
identity: crate::extension_call_authorization::Identity::from_claims(claims)?,
|
||||
path: entry.path().to_owned(),
|
||||
entry: entry.relative_name().to_owned(),
|
||||
tree: entry.tree_sha256().to_owned(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Serial MCP session over an already-authorized native instance. Package trust,
|
||||
//! tool consent/schema validation and registry routing remain Host responsibilities.
|
||||
//! Serial MCP session over an already-authorized native instance. The Host
|
||||
//! approval route still must establish user consent and current installation/trust.
|
||||
use crate::{
|
||||
extension_io::{Event, Pump},
|
||||
extension_process::Running,
|
||||
@@ -93,6 +93,7 @@ pub struct Session<'a, 'p> {
|
||||
failed: bool,
|
||||
tools_changed: bool,
|
||||
catalog: Option<crate::extension_mcp_tools::Catalog>,
|
||||
calls: crate::extension_call_authorization::Gate,
|
||||
}
|
||||
impl<'a, 'p> Session<'a, 'p> {
|
||||
pub fn new(process: &'a Running<'p>, io: HostIo) -> Result<Self> {
|
||||
@@ -105,6 +106,7 @@ impl<'a, 'p> Session<'a, 'p> {
|
||||
failed: false,
|
||||
tools_changed: false,
|
||||
catalog: None,
|
||||
calls: crate::extension_call_authorization::Gate::new(process.call_identity()?),
|
||||
})
|
||||
}
|
||||
pub fn initialize(&mut self, cancel: &AtomicBool) -> Result<String> {
|
||||
@@ -149,6 +151,7 @@ impl<'a, 'p> Session<'a, 'p> {
|
||||
self.require_tools()?;
|
||||
self.drain_pending()?;
|
||||
self.catalog = None;
|
||||
self.calls.invalidate();
|
||||
self.tools_changed = false;
|
||||
let started = Instant::now();
|
||||
let catalog = crate::extension_mcp_tools::Catalog::discover(|cursor| {
|
||||
@@ -201,12 +204,62 @@ impl<'a, 'p> Session<'a, 'p> {
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
pub fn review_call(
|
||||
&mut self,
|
||||
name: &str,
|
||||
arguments: Value,
|
||||
) -> Result<crate::extension_call_authorization::Review> {
|
||||
self.require_tools()?;
|
||||
self.drain_pending()?;
|
||||
let tool = self
|
||||
.catalog
|
||||
.as_ref()
|
||||
.ok_or_else(|| HostError::new("EXTENSION_MCP_CATALOG_REQUIRED"))?
|
||||
.tool(name)?;
|
||||
self.calls.review(&tool, arguments)
|
||||
}
|
||||
/// Only invoke from an authenticated Host route after the user approved this
|
||||
/// exact review. This method is not registered as a renderer/Core command.
|
||||
pub fn confirm_call(
|
||||
&mut self,
|
||||
review_id: &str,
|
||||
) -> Result<crate::extension_call_authorization::ApprovedCall> {
|
||||
self.require_tools()?;
|
||||
self.drain_pending()?;
|
||||
self.calls.confirm(review_id)
|
||||
}
|
||||
pub fn call_tool(
|
||||
&mut self,
|
||||
approved: crate::extension_call_authorization::ApprovedCall,
|
||||
cancel: &AtomicBool,
|
||||
) -> Result<Value> {
|
||||
self.require_tools()?;
|
||||
self.drain_pending()?;
|
||||
let call = self.calls.consume(approved)?;
|
||||
let tool = self
|
||||
.catalog
|
||||
.as_ref()
|
||||
.ok_or_else(|| HostError::new("EXTENSION_MCP_CATALOG_REQUIRED"))?
|
||||
.tool(&call.name)?;
|
||||
if crate::extension_call_authorization::contract_digest(&tool.description())?
|
||||
!= call.contract_digest
|
||||
{
|
||||
return Err(HostError::new("EXTENSION_CALL_CATALOG_CHANGED"));
|
||||
}
|
||||
self.invoke_tool(&call.name, call.arguments, cancel)
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_call_tool(
|
||||
&mut self,
|
||||
name: &str,
|
||||
arguments: Value,
|
||||
cancel: &AtomicBool,
|
||||
) -> Result<Value> {
|
||||
let review = self.review_call(name, arguments)?;
|
||||
let approved = self.confirm_call(&review.review_id)?;
|
||||
self.call_tool(approved, cancel)
|
||||
}
|
||||
fn invoke_tool(&mut self, name: &str, arguments: Value, cancel: &AtomicBool) -> Result<Value> {
|
||||
self.require_tools()?;
|
||||
self.drain_pending()?;
|
||||
if name.is_empty()
|
||||
@@ -256,6 +309,7 @@ impl<'a, 'p> Session<'a, 'p> {
|
||||
} else if method == "notifications/tools/list_changed" {
|
||||
self.tools_changed = true;
|
||||
self.catalog = None;
|
||||
self.calls.invalidate();
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
@@ -294,6 +348,7 @@ impl<'a, 'p> Session<'a, 'p> {
|
||||
}
|
||||
fn abort(&mut self) {
|
||||
self.failed = true;
|
||||
self.calls.invalidate();
|
||||
let _ = self.process.terminate();
|
||||
}
|
||||
fn request(
|
||||
|
||||
@@ -77,6 +77,9 @@ pub struct Tool {
|
||||
output: Option<jsonschema::Validator>,
|
||||
}
|
||||
impl Tool {
|
||||
pub(crate) fn description(&self) -> Description {
|
||||
self.description.clone()
|
||||
}
|
||||
fn parse(value: &Value) -> Result<Self> {
|
||||
bounded(value, 192 * 1024, false)?;
|
||||
let name = value["name"]
|
||||
|
||||
@@ -94,6 +94,8 @@ pub struct Running<'a> {
|
||||
process: Process<'a>,
|
||||
#[cfg(feature = "desktop")]
|
||||
revocation: Option<crate::extension_revocation::Watch>,
|
||||
#[cfg(feature = "desktop")]
|
||||
identity: Option<crate::extension_call_authorization::Identity>,
|
||||
}
|
||||
impl<'a> Suspended<'a> {
|
||||
/// Creates hidden, with no inherited handles and an explicit environment and
|
||||
@@ -244,15 +246,18 @@ impl<'a> Suspended<'a> {
|
||||
process: self.0,
|
||||
#[cfg(feature = "desktop")]
|
||||
revocation: None,
|
||||
#[cfg(feature = "desktop")]
|
||||
identity: None,
|
||||
})
|
||||
}
|
||||
/// # Safety
|
||||
/// The same complete resource/broker/trust preconditions as resume apply.
|
||||
/// This additionally arms revocation monitoring before any instruction resumes.
|
||||
#[cfg(feature = "desktop")]
|
||||
pub unsafe fn resume_with_lease(
|
||||
pub(crate) unsafe fn resume_with_lease(
|
||||
self,
|
||||
lease: crate::extension_permit::Lease,
|
||||
identity: crate::extension_call_authorization::Identity,
|
||||
) -> Result<Running<'a>> {
|
||||
let watch = crate::extension_revocation::Watch::arm(&self.0.job, lease)?;
|
||||
watch.check()?;
|
||||
@@ -262,12 +267,20 @@ impl<'a> Suspended<'a> {
|
||||
let running = Running {
|
||||
process: self.0,
|
||||
revocation: Some(watch),
|
||||
identity: Some(identity),
|
||||
};
|
||||
running.check_authorization()?;
|
||||
Ok(running)
|
||||
}
|
||||
}
|
||||
impl Running<'_> {
|
||||
#[cfg(feature = "desktop")]
|
||||
pub(crate) fn call_identity(&self) -> Result<crate::extension_call_authorization::Identity> {
|
||||
self.check_authorization()?;
|
||||
self.identity
|
||||
.clone()
|
||||
.ok_or_else(|| HostError::new("EXTENSION_CALL_BINDING_REQUIRED"))
|
||||
}
|
||||
#[cfg(feature = "desktop")]
|
||||
pub fn start_io(
|
||||
&self,
|
||||
|
||||
@@ -90,3 +90,6 @@ pub mod extension_mcp;
|
||||
|
||||
#[cfg(all(windows, feature = "desktop"))]
|
||||
pub mod extension_mcp_tools;
|
||||
|
||||
#[cfg(all(windows, feature = "desktop"))]
|
||||
pub mod extension_call_authorization;
|
||||
|
||||
Reference in New Issue
Block a user