mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
feat: WASM extension versioning with WIT compat checks (#592)
* feat: add WASM extension versioning with WIT compat checks and CI enforcement Phase 1 — WIT Versioning & Compatibility Checks: - Version WIT packages as `package near:[email protected];` - Add `semver` crate for version parsing and comparison - Add `WIT_TOOL_VERSION` / `WIT_CHANNEL_VERSION` host constants - Add `version` and `wit_version` fields to capabilities schemas - Add `wit_version` column to `wasm_tools` DB table (both backends) - Add load-time `check_wit_version_compat()` with semver rules - Add `IncompatibleWitVersion` error variants for tools and channels - Enhance instantiation errors with WIT version mismatch hints - Update all 14 capabilities JSON and 14 registry JSON files Phase 2 — Upgrade-in-Place & Channel DB Storage: - Change tool store to DELETE-before-INSERT (one version per extension) - Create `wasm_channels` table (PostgreSQL migration + libSQL schema) - Add `WasmChannelStore` trait with PostgreSQL and libSQL backends - Add `extension_info` tool showing version, WIT version, and status - Wire `ExtensionInfoTool` into tool registry (7 extension tools) Phase 3 — CI Version-Bump Enforcement: - Add `scripts/check-version-bumps.sh` checking WIT/tool/channel versions - Add `version-check` CI job (PR-only) to `.github/workflows/test.yml` - Support `[skip-version-check]` label/commit message bypass Includes 7 regression tests for WIT version compatibility checking and 2 integration tests for WIT version annotation verification. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR review feedback for WASM extension versioning - Wrap PostgreSQL DELETE+INSERT in transactions for both tool and channel store() methods to prevent data loss on partial failure (Gemini, Copilot) - Rename StoredWasmChannelWithBinary.tool → .channel (copy-paste fix) - Remove unused WasmError::IncompatibleWitVersion variant (dead code) - Map channel loader WIT mismatch to IncompatibleWitVersion instead of generic Config error, simplify variant to single String message - Fix extension_info description to match actual returned fields - Add schema test for ExtensionInfoTool matching existing test pattern - Fix CI script to fail fast on git errors instead of silent bypass [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a516e92156
commit
04c5c3fe9f
+84
-22
@@ -214,22 +214,21 @@ fn instantiate_tool_component(
|
||||
|
||||
// If the WIT added/removed/renamed a function, stub registration
|
||||
// or instantiation will fail.
|
||||
{
|
||||
// Register stubs for both versioned (0.2.0+) and unversioned (pre-0.2.0) interface
|
||||
// paths so that both old and new WASM artifacts can instantiate.
|
||||
for interface in &["near:agent/host", "near:agent/[email protected]"] {
|
||||
let mut root = linker.root();
|
||||
let mut host = root
|
||||
.instance("near:agent/host")
|
||||
.map_err(|e| format!("failed to create host instance: {e}"))?;
|
||||
if let Ok(mut host) = root.instance(interface) {
|
||||
stub_shared_host_functions(&mut host)?;
|
||||
|
||||
stub_shared_host_functions(&mut host)?;
|
||||
|
||||
// tool-invoke is only in the tool host interface, not channel-host
|
||||
host.func_new("tool-invoke", |_ctx, _args, results| {
|
||||
results[0] = wasmtime::component::Val::Result(Err(Some(Box::new(
|
||||
wasmtime::component::Val::String("stub".into()),
|
||||
))));
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| format!("stub 'tool-invoke': {e}"))?;
|
||||
host.func_new("tool-invoke", |_ctx, _args, results| {
|
||||
results[0] = wasmtime::component::Val::Result(Err(Some(Box::new(
|
||||
wasmtime::component::Val::String("stub".into()),
|
||||
))));
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| format!("stub 'tool-invoke': {e}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut store = Store::new(engine, TestStoreData::new());
|
||||
@@ -253,15 +252,15 @@ fn instantiate_channel_component(
|
||||
wasmtime_wasi::add_to_linker_sync(&mut linker)
|
||||
.map_err(|e| format!("WASI linker failed: {e}"))?;
|
||||
|
||||
{
|
||||
let mut root = linker.root();
|
||||
let mut host = root
|
||||
.instance("near:agent/channel-host")
|
||||
.map_err(|e| format!("failed to create channel-host instance: {e}"))?;
|
||||
// Register stubs for both versioned (0.2.0+) and unversioned (pre-0.2.0) interface
|
||||
// paths so that both old and new WASM artifacts can instantiate.
|
||||
// Register stubs under both versioned and unversioned interface paths.
|
||||
// This helper avoids repeating the stub registration code.
|
||||
fn stub_channel_host(
|
||||
host: &mut wasmtime::component::LinkerInstance<'_, TestStoreData>,
|
||||
) -> Result<(), String> {
|
||||
stub_shared_host_functions(host)?;
|
||||
|
||||
stub_shared_host_functions(&mut host)?;
|
||||
|
||||
// Channel-specific host functions
|
||||
host.func_new("emit-message", |_ctx, _args, _results| Ok(()))
|
||||
.map_err(|e| format!("stub 'emit-message': {e}"))?;
|
||||
|
||||
@@ -294,6 +293,23 @@ fn instantiate_channel_component(
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| format!("stub 'pairing-read-allow-from': {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
{
|
||||
let mut root = linker.root();
|
||||
let mut host = root
|
||||
.instance("near:agent/channel-host")
|
||||
.map_err(|e| format!("failed to create unversioned channel-host: {e}"))?;
|
||||
stub_channel_host(&mut host)?;
|
||||
}
|
||||
{
|
||||
let mut root = linker.root();
|
||||
let mut host = root
|
||||
.instance("near:agent/[email protected]")
|
||||
.map_err(|e| format!("failed to create versioned channel-host: {e}"))?;
|
||||
stub_channel_host(&mut host)?;
|
||||
}
|
||||
|
||||
let mut store = Store::new(engine, TestStoreData::new());
|
||||
@@ -477,3 +493,49 @@ fn wit_compat_all_registry_extensions_have_source() {
|
||||
missing.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wit_files_contain_version_annotation() {
|
||||
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
for wit_file in &["wit/tool.wit", "wit/channel.wit"] {
|
||||
let path = repo_root.join(wit_file);
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|e| panic!("failed to read {wit_file}: {e}"));
|
||||
|
||||
assert!(
|
||||
content.contains("package near:agent@"),
|
||||
"{wit_file} must contain a versioned package declaration (e.g., 'package near:[email protected];')"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wit_version_constants_match_wit_files() {
|
||||
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
let tool_wit = std::fs::read_to_string(repo_root.join("wit/tool.wit"))
|
||||
.expect("failed to read wit/tool.wit");
|
||||
let channel_wit = std::fs::read_to_string(repo_root.join("wit/channel.wit"))
|
||||
.expect("failed to read wit/channel.wit");
|
||||
|
||||
let expected_tool = format!(
|
||||
"package near:agent@{};",
|
||||
ironclaw::tools::wasm::WIT_TOOL_VERSION
|
||||
);
|
||||
let expected_channel = format!(
|
||||
"package near:agent@{};",
|
||||
ironclaw::tools::wasm::WIT_CHANNEL_VERSION
|
||||
);
|
||||
|
||||
assert!(
|
||||
tool_wit.contains(&expected_tool),
|
||||
"wit/tool.wit version must match WIT_TOOL_VERSION constant ({})",
|
||||
ironclaw::tools::wasm::WIT_TOOL_VERSION
|
||||
);
|
||||
assert!(
|
||||
channel_wit.contains(&expected_channel),
|
||||
"wit/channel.wit version must match WIT_CHANNEL_VERSION constant ({})",
|
||||
ironclaw::tools::wasm::WIT_CHANNEL_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user