feat(plugins): add AI tools and consent-gated data access

This commit is contained in:
t8y2 committed 2026-09-25 01:56:26 +08:00
1 parent 1bc6f70cd8
commit 26a015cd86
71 files changed
+4613 -129

No files matched your search

@@ -53,6 +53,15 @@ impl PluginConnectionHandle {
self.session.as_ref().is_none_or(|session| session.status().state == PluginSessionState::Running)
}
/// The lifecycle payload this connection was opened with: provider,
/// hydrated connection, and the runtime endpoint *after* DBX transport
/// layers. Host-initiated calls that act on an open connection (for
/// example plugin MCP tools) must reuse it instead of rebuilding a payload
/// from the saved config, which would bypass the tunnel.
pub fn lifecycle_params(&self) -> &serde_json::Value {
&self.params
}
pub async fn disconnect(&self) -> Result<(), String> {
let Some(session) = &self.session else {
return Ok(());
@@ -13,10 +13,12 @@ pub const SUPPORTED_PLUGIN_MANIFEST_VERSION: u32 = 1;
/// schema metadata (`host.getTableMetadata` behind `host.schema:read`) and the
/// plugin-initiated clipboard Host API (`host.clipboardRead` behind the
/// `host.clipboard:read` permission; clipboard writes reuse the existing
/// ungated `host.copy`). All are additive: older plugins keep working, and a
/// plugin that wants a capability must check the advertised version (or the
/// matching `capabilities` / `host.features` entry) before calling it.
pub const SUPPORTED_PLUGIN_HOST_API_VERSION: &str = "1.3.0";
/// ungated `host.copy`). 1.4 adds consent-gated read-only data queries
/// (`host.queryData` behind `host.data:read`). All are additive: older plugins
/// keep working, and a plugin that wants a capability must check the
/// advertised version (or the matching `capabilities` / `host.features` entry)
/// before calling it.
pub const SUPPORTED_PLUGIN_HOST_API_VERSION: &str = "1.4.0";
/// Capabilities the host advertises to a plugin backend at `plugin/initialize`.
pub const SUPPORTED_PLUGIN_HOST_FEATURES: &[&str] = &["host.requestUserInput"];
pub const SUPPORTED_PLUGIN_PROTOCOL_VERSION: u32 = 1;
@@ -34,6 +36,7 @@ pub const SUPPORTED_PLUGIN_PERMISSIONS: &[&str] = &[
"host.storage",
"host.ai",
"host.clipboard:read",
"host.data:read",
];
/// Cap the number of `host.network:<origin>` entries so a manifest cannot bloat
@@ -2240,27 +2243,27 @@ mod tests {
assert_eq!(declared, SUPPORTED_PLUGIN_PERMISSIONS.iter().map(|value| value.to_string()).collect::<Vec<_>>());
}
/// The reason for the 1.3.0 bump: `engines.host_api` is how a plugin states
/// "I need the schema metadata API" or "I need clipboard reads", so the
/// advertised version has to satisfy `^1.3` while a floor this host cannot
/// meet stays rejected.
/// `engines.host_api` is how a plugin states "I need the schema metadata
/// API" / "I need clipboard reads" (1.3) or "I need data queries" (1.4), so
/// the advertised version has to satisfy each floor while a floor this host
/// cannot meet stays rejected.
#[test]
fn host_api_advertises_the_floor_a_schema_metadata_plugin_declares() {
let advertised = semver::Version::parse(SUPPORTED_PLUGIN_HOST_API_VERSION)
.expect("the advertised Host API version must be semver");
assert!(
semver::VersionReq::parse("^1.3").unwrap().matches(&advertised),
"the host must satisfy the schema metadata API floor it asks plugins to declare"
"the host must satisfy the schema metadata and clipboard-read floor it asks plugins to declare"
);
assert!(
semver::VersionReq::parse("^1.3").unwrap().matches(&advertised),
"the host must satisfy the clipboard-read floor it asks plugins to declare"
semver::VersionReq::parse("^1.4").unwrap().matches(&advertised),
"the host must satisfy the data-query floor it asks plugins to declare"
);
for requirement in ["^1.0", "^1.1", "^1.2", "^1.3", ">=1.1.0, <2.0.0"] {
for requirement in ["^1.0", "^1.1", "^1.2", "^1.3", "^1.4", ">=1.1.0, <2.0.0"] {
assert!(host_api_requirement_errors(requirement).is_empty(), "{requirement} must be satisfiable");
}
for requirement in [">=1.4.0", "^2.0"] {
for requirement in [">=1.5.0", "^2.0"] {
assert!(!host_api_requirement_errors(requirement).is_empty(), "{requirement} must be rejected");
}
}