plugin: bound component model runtime resources
This commit is contained in:
parent
57bbf14e1a
commit
a705bb3bf2
|
|
@ -1511,6 +1511,17 @@ const PLUGIN_FS_MAX_READ_BYTES: usize = 64 * 1024;
|
|||
const PLUGIN_FS_MAX_WRITE_BYTES: usize = 64 * 1024;
|
||||
const PLUGIN_FS_MAX_LIST_ENTRIES: usize = 256;
|
||||
|
||||
fn wasm_component_store_limits() -> wasmtime::StoreLimits {
|
||||
wasmtime::StoreLimitsBuilder::new()
|
||||
.memory_size(PLUGIN_WASM_MEMORY_BYTES)
|
||||
.table_elements(PLUGIN_WASM_TABLE_ELEMENTS)
|
||||
.instances(1)
|
||||
.tables(1)
|
||||
.memories(1)
|
||||
.trap_on_grow_failure(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum PluginFsRuntimeOperation {
|
||||
Read,
|
||||
|
|
@ -1916,6 +1927,7 @@ fn run_plugin_wasm_tool_with_https_client(
|
|||
struct PluginComponentHostState {
|
||||
record: ResolvedPluginRecord,
|
||||
https_client: Arc<dyn PluginHttpsClient>,
|
||||
store_limits: wasmtime::StoreLimits,
|
||||
}
|
||||
|
||||
fn run_plugin_component_tool(
|
||||
|
|
@ -1981,8 +1993,10 @@ fn run_plugin_component_tool_with_https_client(
|
|||
PluginComponentHostState {
|
||||
record: record.clone(),
|
||||
https_client,
|
||||
store_limits: wasm_component_store_limits(),
|
||||
},
|
||||
);
|
||||
store.limiter(|state| &mut state.store_limits);
|
||||
store
|
||||
.set_fuel(PLUGIN_WASM_FUEL)
|
||||
.map_err(|error| PluginWasmError::Execution(error.to_string()))?;
|
||||
|
|
@ -2000,6 +2014,12 @@ fn run_plugin_component_tool_with_https_client(
|
|||
let input_json = std::str::from_utf8(&input).map_err(|error| {
|
||||
PluginWasmError::Output(format!("plugin component input is not UTF-8: {error}"))
|
||||
})?;
|
||||
// Wasmtime lifts the returned WIT `string` into a host `String` before the
|
||||
// ordinary ToolOutput JSON cap can be applied. Keep the component store on
|
||||
// the same memory/table/instance limits as the raw WASM runtime so an
|
||||
// untrusted component can only force host string allocation from bounded
|
||||
// component memory; oversized memories/tables/instances fail closed during
|
||||
// instantiation/growth before this lift succeeds.
|
||||
let (output,) = call
|
||||
.call(&mut store, (&tool_name, input_json))
|
||||
.map_err(|error| PluginWasmError::Execution(error.to_string()))?;
|
||||
|
|
@ -4070,10 +4090,14 @@ input_schema = {{ type = "object", additionalProperties = true }}
|
|||
}
|
||||
|
||||
fn component_tool_that_returns(output: &[u8]) -> Vec<u8> {
|
||||
component_tool_with_memory_pages(output, 1)
|
||||
}
|
||||
|
||||
fn component_tool_with_memory_pages(output: &[u8], memory_pages: usize) -> Vec<u8> {
|
||||
wat::parse_str(format!(
|
||||
r#"(component
|
||||
(core module $m
|
||||
(memory (export "memory") 1)
|
||||
(memory (export "memory") {})
|
||||
(func (export "realloc") (param i32 i32 i32 i32) (result i32)
|
||||
(if (result i32) (i32.eqz (local.get 0))
|
||||
(then (i32.const 8192))
|
||||
|
|
@ -4092,6 +4116,38 @@ input_schema = {{ type = "object", additionalProperties = true }}
|
|||
(func $call (type $call_ty) (canon lift (core func $call_core) (memory $mem) (realloc $realloc) string-encoding=utf8))
|
||||
(export "call" (func $call))
|
||||
)"#,
|
||||
memory_pages,
|
||||
wat_bytes(output),
|
||||
output.len()
|
||||
))
|
||||
.expect("valid component wat")
|
||||
}
|
||||
|
||||
fn component_tool_with_table_elements(output: &[u8], table_elements: usize) -> Vec<u8> {
|
||||
wat::parse_str(format!(
|
||||
r#"(component
|
||||
(core module $m
|
||||
(memory (export "memory") 1)
|
||||
(table {} funcref)
|
||||
(func (export "realloc") (param i32 i32 i32 i32) (result i32)
|
||||
(if (result i32) (i32.eqz (local.get 0))
|
||||
(then (i32.const 8192))
|
||||
(else (local.get 0))))
|
||||
(data (i32.const 1024) "{}")
|
||||
(func (export "call") (param i32 i32 i32 i32) (result i32)
|
||||
(i32.store (i32.const 2048) (i32.const 1024))
|
||||
(i32.store (i32.const 2052) (i32.const {}))
|
||||
(i32.const 2048))
|
||||
)
|
||||
(core instance $i (instantiate $m))
|
||||
(alias core export $i "memory" (core memory $mem))
|
||||
(alias core export $i "realloc" (core func $realloc))
|
||||
(alias core export $i "call" (core func $call_core))
|
||||
(type $call_ty (func (param "tool-name" string) (param "input-json" string) (result string)))
|
||||
(func $call (type $call_ty) (canon lift (core func $call_core) (memory $mem) (realloc $realloc) string-encoding=utf8))
|
||||
(export "call" (func $call))
|
||||
)"#,
|
||||
table_elements,
|
||||
wat_bytes(output),
|
||||
output.len()
|
||||
))
|
||||
|
|
@ -4150,6 +4206,48 @@ input_schema = {{ type = "object", additionalProperties = true }}
|
|||
assert_eq!(output.content.as_deref(), Some("ordinary tool result path"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_memory_limit_fails_closed_before_string_lift() {
|
||||
let oversized_memory_pages = (PLUGIN_WASM_MEMORY_BYTES / 65_536) + 1;
|
||||
let (_dir, record) = resolved_record_with_component(component_tool_with_memory_pages(
|
||||
br#"{"summary":"should not lift"}"#,
|
||||
oversized_memory_pages,
|
||||
));
|
||||
|
||||
let error = run_plugin_component_tool(record, "PluginEcho".to_string(), b"{}".to_vec())
|
||||
.expect_err("component memory limit is enforced");
|
||||
|
||||
assert!(format!("{error:?}").contains("growing memory"), "{error:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_table_limit_fails_closed() {
|
||||
let (_dir, record) = resolved_record_with_component(component_tool_with_table_elements(
|
||||
br#"{"summary":"should not run"}"#,
|
||||
PLUGIN_WASM_TABLE_ELEMENTS + 1,
|
||||
));
|
||||
|
||||
let error = run_plugin_component_tool(record, "PluginEcho".to_string(), b"{}".to_vec())
|
||||
.expect_err("component table limit is enforced");
|
||||
|
||||
assert!(format!("{error:?}").contains("growing table"), "{error:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_output_cap_still_fails_closed_after_bounded_lift() {
|
||||
let output = format!(
|
||||
r#"{{"summary":"too big","content":"{}"}}"#,
|
||||
"x".repeat(PLUGIN_WASM_MAX_OUTPUT_BYTES)
|
||||
);
|
||||
let (_dir, record) =
|
||||
resolved_record_with_component(component_tool_with_memory_pages(output.as_bytes(), 2));
|
||||
|
||||
let error = run_plugin_component_tool(record, "PluginEcho".to_string(), b"{}".to_vec())
|
||||
.expect_err("component output cap is enforced");
|
||||
|
||||
assert!(format!("{error:?}").contains("output exceeds"), "{error:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_tool_denies_host_import_without_matching_grant() {
|
||||
let (_dir, record) = resolved_record_with_component(component_tool_importing_https(
|
||||
|
|
|
|||
|
|
@ -165,3 +165,13 @@ See `docs/examples/plugin-component-tool/lib.rs` for a minimal
|
|||
`wit-bindgen`/SDK-style authoring sketch. Package authors should generate
|
||||
bindings from `resources/plugin/wit`, build a component artifact, and set the
|
||||
component runtime metadata above.
|
||||
|
||||
### v1 request/response shape
|
||||
|
||||
The v1 component world intentionally keeps Tool input, Tool output, and host API
|
||||
payloads as JSON strings. This is a migration bridge that preserves the existing
|
||||
ToolOutput schema, Tool history behavior, grant checks, and raw-Wasm host API
|
||||
semantics while moving package authors onto WIT/canonical ABI bindings.
|
||||
Structured WIT records for Tool requests/responses/errors and host HTTPS/FS
|
||||
payloads are deferred to a follow-up API-design step rather than accidentally
|
||||
omitted.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user