feat(plugin): expose a read-only schema metadata host API

This commit is contained in:
jearhe authored and GitHub committed 2026-09-23 12:51:23 +08:00
1 parent 2f960f104c
commit d5a05a9884
40 files changed
+1445 -86

No files matched your search

@@ -9,11 +9,12 @@ pub const SUPPORTED_PLUGIN_MANIFEST_VERSION: u32 = 1;
///
/// 1.1 adds the plugin-initiated `host/requestUserInput` method (see
/// `plugins/runtime.rs`). 1.2 adds the plugin-initiated plan Host API
/// (`host.getPlanCapabilities` / `host.explainPlan`). Both are additive: older
/// plugins keep working, and a plugin that wants either capability must check
/// the advertised version (or the matching `capabilities` / `host.features`
/// (`host.getPlanCapabilities` / `host.explainPlan`). 1.3 adds read-only table
/// schema metadata (`host.getTableMetadata`). All are additive: older plugins
/// keep working, and a plugin that wants either 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.2.0";
pub const SUPPORTED_PLUGIN_HOST_API_VERSION: &str = "1.3.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;
@@ -21,8 +22,16 @@ pub const PLUGIN_CONNECTION_TEST_METHOD: &str = "connection/test";
pub const PLUGIN_CONNECTION_CONNECT_METHOD: &str = "connection/connect";
pub const PLUGIN_CONNECTION_DISCONNECT_METHOD: &str = "connection/disconnect";
pub const PLUGIN_CONNECTION_ACTION_METHOD: &str = "connection/action";
pub const SUPPORTED_PLUGIN_PERMISSIONS: &[&str] =
&["host.events", "host.binary", "host.workbench", "host.filesystem", "host.plans:read", "host.storage", "host.ai"];
pub const SUPPORTED_PLUGIN_PERMISSIONS: &[&str] = &[
"host.events",
"host.binary",
"host.workbench",
"host.filesystem",
"host.plans:read",
"host.schema:read",
"host.storage",
"host.ai",
];
/// Cap the number of `host.network:<origin>` entries so a manifest cannot bloat
/// the sandbox CSP or enumerate large origin lists.
@@ -1572,7 +1581,7 @@ mod tests {
}
#[test]
fn accepts_host_plans_read_and_still_rejects_unknown_permissions() {
fn accepts_read_only_host_permissions_and_still_rejects_unknown_permissions() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("ui")).unwrap();
std::fs::write(dir.path().join("ui").join("index.html"), "<!doctype html>").unwrap();
@@ -1585,14 +1594,22 @@ mod tests {
"publisher": "example",
"engines": { "dbx": ">=0.1.0", "host_api": "^1.0" },
"entrypoints": { "ui": { "root": "ui", "entry": "ui/index.html" } },
"permissions": ["host.plans:read"]
"permissions": ["host.plans:read", "host.schema:read"]
}))
.unwrap();
let compatibility = manifest.compatibility(dir.path(), "0.1.0");
assert!(compatibility.compatible, "{:?}", compatibility.errors);
// The plan API is read-only: an execute-scoped scope must not be declared.
for permission in ["host.plans:execute", "host.plans", "host.plans:read:all", "host.plan:read"] {
// Both the plan and schema metadata APIs are read-only; execute/write
// scopes must not be declared as substitutes.
for permission in [
"host.plans:execute",
"host.plans",
"host.plans:read:all",
"host.plan:read",
"host.schema:write",
"host.schema",
] {
let manifest: PluginManifest = serde_json::from_value(serde_json::json!({
"manifest_version": 1,
"id": "io.dbx.example",
@@ -1635,22 +1652,22 @@ mod tests {
assert_eq!(declared, SUPPORTED_PLUGIN_PERMISSIONS.iter().map(|value| value.to_string()).collect::<Vec<_>>());
}
/// The reason for the 1.2.0 bump: `engines.host_api` is how a plugin states
/// "I need the plan API", so the advertised version has to satisfy `^1.2`
/// while a floor this host cannot meet stays rejected.
/// The reason for the 1.3.0 bump: `engines.host_api` is how a plugin states
/// "I need the schema metadata API", so the advertised version has to
/// satisfy `^1.3` while a floor this host cannot meet stays rejected.
#[test]
fn host_api_advertises_the_floor_a_plan_api_plugin_declares() {
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.2").unwrap().matches(&advertised),
"the host must satisfy the plan API floor it asks plugins to declare"
semver::VersionReq::parse("^1.3").unwrap().matches(&advertised),
"the host must satisfy the schema metadata API floor it asks plugins to declare"
);
for requirement in ["^1.0", "^1.1", "^1.2", ">=1.1.0, <2.0.0"] {
for requirement in ["^1.0", "^1.1", "^1.2", "^1.3", ">=1.1.0, <2.0.0"] {
assert!(host_api_requirement_errors(requirement).is_empty(), "{requirement} must be satisfiable");
}
for requirement in [">=1.3.0", "^2.0"] {
for requirement in [">=1.4.0", "^2.0"] {
assert!(!host_api_requirement_errors(requirement).is_empty(), "{requirement} must be rejected");
}
}