feat: WASM-based language execution runtime#707
feat: WASM-based language execution runtime#707HexaField wants to merge 27 commits intocoasys:devfrom
Conversation
- Baseline profiling: 355 MB startup, 750 MB post-init, ~78 MB per neighbourhood - Leak investigation: 0% memory recovery on neighbourhood teardown - perspectiveRemove does not uninstall Holochain hApps or free WASM runtimes - Bare perspectives leak ~2.4 MB each, language cloning leaks ~4.2 MB each - Includes reproduction scripts (profiler, leak tester, publish-langs)
Detailed code-level analysis tracing all three categories of memory leaks: 1. CRITICAL: Neighbourhood teardown leaks 100% - perspectiveRemove only sets a flag, never uninstalls Holochain hApps, Prolog pools, SurrealDB, or JS languages 2. Bare perspectives leak ~2.4 MB each (Prolog pools + SurrealDB not freed) 3. Language cloning leaks ~4.2 MB per clone (permanent, no unload path) Includes exact file/line references, proposed fixes ordered by priority, and architecture recommendations (lifecycle contract, reference counting).
CRITICAL fixes: - Fix 1: Proper teardown_background_tasks that cleans up Prolog pools, SurrealDB, link language, subscribed queries, and batch store - Fix 2: Add language_remove method to Rust LanguageController to call JS languageController.languageRemove() during teardown - Fix 3: Clean up Holochain signal callbacks on language removal (both JS #signalCallbacks and Rust signal stream StreamMap) - Rename _remove_perspective_pool to remove_perspective_pool MEDIUM fixes: - Fix 4: Add reference counting for languages in LanguageController.ts (languageAddRef/languageReleaseRef) - Fix 5: Add SurrealDB shutdown() method that drops all data and indexes
Baseline vs patched binary comparison confirms: - AD4M-layer teardown works correctly (SurrealDB, signals, languages) - Holochain conductor retains ~140MB/neighbourhood after uninstall_app - 0% memory recovery on both original and patched binaries - Root cause is conductor-level wasmer/LMDB memory management Updated leak-investigation.mjs with v2 improvements: - Fixed GQL schema for DecoratedLinkExpression - Added detailed smaps breakdown per test phase - Added large anon mapping tracking across lifecycle - Added teardown log verification
- Remove languageAddRef/languageReleaseRef and #languageRefCounts from LanguageController.ts — these were never called from any code path - Simplify SurrealDB shutdown() to just log — SurrealDB uses in-memory storage (Surreal::new::<Mem>), so explicit DELETE/REMOVE INDEX is unnecessary; memory is freed when the Arc<Surreal<Db>> is dropped
Adds a WASM language runtime that enables AD4M Language modules to be compiled to WebAssembly and executed in the Wasmer runtime (same engine Holochain uses). This eliminates the need for V8/Deno for languages that target WASM, reducing per-language memory overhead. Components: - rust-executor/src/wasm_core/ — WASM loader, ABI, host functions, registry - wasm-language-sdk/ — Rust SDK crate for language authors (types, traits, macros) - examples/wasm-languages/note-store/ — port of note-store language to Rust/WASM Key design: - ABI versioned from day one (AD4M_LANGUAGE_ABI_VERSION = 1) - Fat pointer encoding (u64) for passing data across WASM boundary - JSON serialisation for structured data - Per-language isolation (each gets own WASM instance + linear memory) - Host functions mirror Deno ops: agent_did, agent_sign, hash, etc. - Feature-gated: cargo check --features wasm-languages - Does not break existing Deno/JS language path The example note-store language compiles to a 119KB WASM binary with all required exports (ad4m_alloc, ad4m_dealloc, ad4m_expression_get, etc.) and imports only the host functions it actually uses.
- Add LanguageBackend async trait in languages/language.rs abstracting sync, commit, current_revision, render, others, telepresence methods - Implement LanguageBackend for existing JS Language (unchanged behavior) - Add WasmLanguage backend (feature-gated behind wasm-languages) wrapping WasmLanguageInstance with sensible defaults for unimplemented methods - Update LanguageController::language_by_address to check WASM registry first, falling back to JS - Add install_wasm_language and is_wasm_bundle helpers (wasm-languages) - Update language_remove to handle WASM languages - Update perspective_instance.rs to use Arc<Mutex<dyn LanguageBackend>> instead of concrete Language type - Add async-trait dependency, fix duplicate surrealdb dep in Cargo.toml
- Fix schema.gql symlink (core/lib/src -> tests/js) - Fix AgentContext/did_for_context/sign_for_context -> agent::did()/sign() - Fix create_signed_expression to use 1-arg API - Remove conflicting From<WasmLanguageError> impl (blanket covers it) - Fix perspective_instance to use Box<dyn LanguageBackend> in Arc<Mutex<>> - Add set_app_data_path to perspectives/mod.rs (merge gap) - Forward wasm-languages feature through cli/Cargo.toml
- Add LinksAdapter trait to wasm-language-sdk with sync/commit/render/current_revision/others - Add ad4m_links_adapter! macro for optional WASM export generation - Add has_links_adapter capability detection in host - Add sync/commit/render/current_revision/others methods to WasmLanguageInstance - Wire WasmLanguage backend to call through to WASM instance methods - Add WASM bundle detection in JS LanguageController (magic bytes check) - Add WASM install path in Rust LanguageController.install_language
…9 pass) - New example: link-store WASM language with full LinksAdapter (sync, commit, render, current_revision, others) - Fix HOST_MODULE_NAME: "ad4m" -> "env" to match extern "C" default imports - Remove duplicate inline mod tests from wasm_core/mod.rs - 7 new LinksAdapter tests + rebuilt WASM fixtures
…terface - Update AbiHcCallRequest: replace dna_hash/agent_pubkey with dna_nick - Add tokio_handle to HostEnv for sync->async bridging - Implement host_hc_call using block_in_place + handle.block_on - Use maybe_get_holochain_service() for defensive error handling - Update SDK: new holochain_call(dna_nick, zome_name, fn_name, payload) API - Deprecate old hc_call() in SDK
… and ad4m_init lifecycle hook - Add hc_install_app, hc_remove_app, hc_get_agent_key host functions to wasm_core - Register new host functions in WASM imports - Add ad4m_init lifecycle hook: called after WASM instantiation for DNA setup - Add SDK bindings: holochain_install_app, holochain_remove_app, holochain_get_agent_key - Add LanguageInit trait with default no-op init() method - Generate ad4m_init export in ad4m_language! macro
- New p-diff-sync-wasm example: real Holochain-backed link language - Embeds 1.1MB Perspective-Diff-Sync .happ bundle via include_bytes! - Implements full LinksAdapter (sync, commit, render, current_revision, others) - Uses rmp-serde for msgpack serialization to match zome ABI - DNA installed via ad4m_init lifecycle hook - All zome calls proxied through holochain_call host function - Fix SDK macro: ad4m_teardown was missing closing brace (ad4m_init nested inside) - Make tokio Handle optional in HostEnv (Handle::try_current) - Allows WASM tests to run without tokio runtime - Host functions gracefully return null when no runtime available - 17/17 WASM tests passing (p-diff-sync correctly fails without conductor) - Compiled WASM: 1.4MB (1.1MB DNA + ~300KB code)
- Fix snapshot not being re-embedded (add cargo:rerun-if-changed to build.rs) - Restore is_initialized() guard in agent_load() to prevent crash on fresh data - Add install_wasm_language op to languages extension (JS + Rust) - Add languageInstallWasm GQL mutation for WASM language installation - Route expressionCreate/expressionRaw through WASM backend when applicable - Fix misleading comments about host module namespace (env, not ad4m) All 21 WASM unit tests passing. Integration test: agent gen, perspective CRUD, WASM install, expression ops all working.
- Add app_data_path to LanguageController for Rust-native path resolution - Implement install_language WASM detection: checks local bundle.wasm, then fetches from language language and detects base64-encoded WASM (AGFzbQ magic prefix), then falls back to JS install - Add install_wasm_from_base64: decodes, verifies WASM magic, saves to languages dir, registers in WASM runtime - Add publish_wasm_language: base64-encodes WASM binary, adds bundleType:wasm to meta, publishes via language language - Add languagePublishWasm GQL mutation - language_source query returns base64 WASM for WASM languages - Integration test v4: 10/10 tests passing (install, expressions, source query, perspective links, publish, base64 detection, memory) - 21/21 WASM unit tests passing
- Add LanguageInit impl to note-store and link-store examples (macro requires it) - Add rustup default stable to container-based CI jobs (coasys/ad4m-ci-linux container lacks default toolchain)
- Fix p-diff-sync teardown to use stored app_id instead of agent DID - Error on invalid meta JSON in publish_wasm_language instead of silent fallback - Delete bundle files on WASM language removal - Fix CI workflow: use github.head_ref for PR branch detection
The coasys/ad4m-ci-linux container was timing out (1h35m) on GitHub Actions runners. Switch to installing deps directly — matches what the WASM SDK job already does successfully.
bundle.js and CUSTOM_DENO_SNAPSHOT.bin are embedded at compile time but only built by the JS build step. Create placeholder files so cargo check can pass without a full JS build.
surrealdb-rocksdb takes 50+ min to compile from source on free runners. The container image has it pre-built. Keep WASM SDK on bare runner (fast). Bump timeout to 120min for container pull + compile.
📝 WalkthroughWalkthroughThis PR introduces WebAssembly language runtime support to AD4M, enabling languages to be compiled as WASM modules. It includes a new SDK for building WASM languages, a runtime loader with host function bindings, example language implementations, executor integration via GraphQL mutations, comprehensive memory profiling tooling, and cleanup fixes for perspective and Holochain teardown. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as GraphQL Client
participant Executor as Executor (Rust)
participant WasmMgr as WASM Manager
participant Runtime as Wasmer Runtime
participant WasmLang as WASM Language<br/>(Guest)
participant Host as Host Functions
participant Agent as Agent/Signing
Client->>Executor: language_install_wasm(path, address)
Executor->>WasmMgr: load_wasm_language(path, address)
WasmMgr->>Runtime: compile & instantiate WASM
Runtime->>WasmLang: create instance
WasmLang->>Host: ad4m_init() [optional]
Host->>Agent: validate/setup
Agent-->>WasmLang: ok
WasmLang-->>Runtime: initialized
Runtime-->>WasmMgr: WasmLanguageInstance
WasmMgr->>WasmMgr: register in WASM_LANGUAGE_REGISTRY
WasmMgr-->>Executor: success
Executor-->>Client: address
sequenceDiagram
participant App as Application
participant Perspective as PerspectiveInstance
participant Backend as LanguageBackend (Trait)
participant WasmLang as WasmLanguage
participant Guest as WASM Guest
participant HostEnv as Host Environment
App->>Perspective: commit(diff)
Perspective->>Backend: commit(diff)
alt WASM Language
Backend->>WasmLang: commit(diff)
WasmLang->>Guest: ad4m_commit(diff_ptr)
Guest->>HostEnv: serialize & call host
HostEnv->>HostEnv: deserialize diff
HostEnv-->>Guest: result_ptr
Guest->>WasmLang: fat pointer decoded
WasmLang-->>Backend: Result<revision>
else JS Language
Backend->>Backend: invoke JS via Deno
end
Backend-->>Perspective: revision
Perspective-->>App: updated state
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
rust-executor/src/js_core/languages_extension.js (1)
1-4:⚠️ Potential issue | 🔴 CriticalImport the WASM install op before invoking it.
Line 24 calls
install_wasm_language(wasmPath, address), but it is not imported in Lines 1-4, so this will throw aReferenceErrorwhen the method is invoked.🔧 Proposed fix
import { perspective_diff_received, sync_state_changed, telepresence_signal_received, - register_holochain_signal_handler, ad4m_signal_emitted + register_holochain_signal_handler, ad4m_signal_emitted, install_wasm_language } from 'ext:core/ops';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@rust-executor/src/js_core/languages_extension.js` around lines 1 - 4, The function install_wasm_language is being called but not imported, causing a ReferenceError; add install_wasm_language to the top import list (with perspective_diff_received, sync_state_changed, telepresence_signal_received, register_holochain_signal_handler, ad4m_signal_emitted) from the same module or the correct module that exports it, and verify the exported name matches (e.g., install_wasm_language) so the call to install_wasm_language(wasmPath, address) resolves correctly.rust-executor/src/js_core/languages_extension.rs (1)
75-79:⚠️ Potential issue | 🔴 CriticalRegister
install_wasm_languagein the extension ops list.
install_wasm_languageis defined at lines 83–98 with#[op2]decorator but missing from theopslist indeno_core::extension!(line 77). Without this registration, the JS layer cannot import or call this operation.🔧 Proposed fix
deno_core::extension!( language_service, - ops = [perspective_diff_received, sync_state_changed, telepresence_signal_received, register_holochain_signal_handler, ad4m_signal_emitted], + ops = [ + perspective_diff_received, + sync_state_changed, + telepresence_signal_received, + register_holochain_signal_handler, + ad4m_signal_emitted, + install_wasm_language, + ], esm_entry_point = "ext:language_service/languages_extension.js", esm = [dir "src/js_core", "languages_extension.js"] );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@rust-executor/src/js_core/languages_extension.rs` around lines 75 - 79, The extension macro invocation language_service is missing the install_wasm_language op, so add "install_wasm_language" to the ops list inside deno_core::extension! so the #[op2] function install_wasm_language (defined below) is registered and available to the JS layer; update the ops array that currently lists perspective_diff_received, sync_state_changed, telepresence_signal_received, register_holochain_signal_handler, ad4m_signal_emitted to include install_wasm_language as an additional entry.rust-executor/src/languages/language.rs (1)
181-193:⚠️ Potential issue | 🔴 CriticalEscape
remote_agent_didbefore embedding into JS sourceLine 190 injects
remote_agent_diddirectly into a JS string literal. Unescaped input can break the script and opens an injection path.Suggested fix
pub async fn send_signal( &mut self, remote_agent_did: String, payload: PerspectiveExpression, ) -> Result<(), AnyError> { let controller = LanguageController::global_instance(); let payload_json = serde_json::to_string(&payload)?; + let did_json = serde_json::to_string(&remote_agent_did)?; let script = format!( r#" - language.telepresenceAdapter ? await language.telepresenceAdapter.sendSignal("{}", {}) : null + language.telepresenceAdapter ? await language.telepresenceAdapter.sendSignal({}, {}) : null "#, - remote_agent_did, payload_json + did_json, payload_json );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@rust-executor/src/languages/language.rs` around lines 181 - 193, In send_signal, remote_agent_did is interpolated directly into a JS string literal causing injection; escape it before embedding by serializing/escaping the DID (e.g., call serde_json::to_string(&remote_agent_did) and use that escaped string in the script) and then build the script using the escaped value instead of inserting remote_agent_did raw; update the construction of script in send_signal so the remote_agent_did is safe for JS string context.
🟠 Major comments (15)
docs/profiling/profiler-v9.mjs-84-245 (1)
84-245:⚠️ Potential issue | 🟠 MajorUse
try/finallyto guarantee child-process cleanup on failure paths.
bootstrap/executor teardown runs only on the happy path. Any early throw can leave orphaned processes running, which skews later profiling runs.♻️ Suggested structure with guaranteed cleanup
async function main() { writeFileSync(OUT, ""); log("=== AD4M Profiler v9 — With Published Languages ==="); + let bootstrap; + let proc; + let ws; + let execPid; - const bootstrap = execCb(`${HOME}/.cargo/bin/kitsune2-bootstrap-srv`, { maxBuffer: 10*1024*1024 }); + try { + bootstrap = execCb(`${HOME}/.cargo/bin/kitsune2-bootstrap-srv`, { maxBuffer: 10*1024*1024 }); ... - const proc = execCb(cmd, { maxBuffer: 200*1024*1024, cwd: CWD }); + proc = execCb(cmd, { maxBuffer: 200*1024*1024, cwd: CWD }); ... - const ws = new WebSocket(`ws://127.0.0.1:${PORT}/graphql`, "graphql-transport-ws"); + ws = new WebSocket(`ws://127.0.0.1:${PORT}/graphql`, "graphql-transport-ws"); ... - ws.close(); - try { process.kill(execPid, "SIGTERM"); } catch {} - await sleep(2000); - try { process.kill(execPid, "SIGKILL"); } catch {} - try { process.kill(proc.pid, "SIGKILL"); } catch {} - try { bootstrap.kill("SIGTERM"); } catch {} + } finally { + try { ws?.close(); } catch {} + try { if (execPid) process.kill(execPid, "SIGTERM"); } catch {} + await sleep(2000); + try { if (execPid) process.kill(execPid, "SIGKILL"); } catch {} + try { if (proc?.pid) process.kill(proc.pid, "SIGKILL"); } catch {} + try { bootstrap?.kill("SIGTERM"); } catch {} + } }Also applies to: 247-247
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/profiling/profiler-v9.mjs` around lines 84 - 245, The main function currently starts child processes (bootstrap via bootstrap, executor via proc, and a WebSocket ws) but only performs teardown on the happy path; wrap the lifecycle after starting bootstrap (and after proc/ws creation) in a try { ... } finally { ... } block so cleanup always runs: move the bulk of the work into the try and in finally ensure ws.close() is called (if defined), kill bootstrap (bootstrap.kill or bootstrap.kill("SIGTERM") / fallback SIGKILL), kill the executor process by PID (use execPid if set, otherwise proc.pid) with SIGTERM then SIGKILL fallback, and ensure any other resources (e.g., timers) are cleared; reference variables: main, bootstrap, proc, execPid, ws, and the existing process.kill/bootstrap.kill calls to implement guaranteed cleanup.docs/profiling/leak-investigation.mjs-139-406 (1)
139-406:⚠️ Potential issue | 🟠 MajorGuarantee teardown with
try/finallyfor subprocess-heavy profiling runs.Any exception before the end-of-function cleanup can leave bootstrap/executor processes alive and taint subsequent measurements.
♻️ Suggested cleanup hardening
async function main() { writeFileSync(OUT, ""); + let bootstrap; + let proc; + let ws; + let execPid; - const bootstrap = execCb(`${HOME}/.cargo/bin/kitsune2-bootstrap-srv`, { maxBuffer: 10*1024*1024 }); + try { + bootstrap = execCb(`${HOME}/.cargo/bin/kitsune2-bootstrap-srv`, { maxBuffer: 10*1024*1024 }); ... - const proc = execCb(cmd, { maxBuffer: 200*1024*1024, cwd: CWD }); + proc = execCb(cmd, { maxBuffer: 200*1024*1024, cwd: CWD }); ... - const ws = new WebSocket(`ws://127.0.0.1:${PORT}/graphql`, "graphql-transport-ws"); + ws = new WebSocket(`ws://127.0.0.1:${PORT}/graphql`, "graphql-transport-ws"); ... - ws.close(); - try { process.kill(execPid, "SIGTERM"); } catch {} - await sleep(3000); - try { process.kill(execPid, "SIGKILL"); } catch {} - try { process.kill(proc.pid, "SIGKILL"); } catch {} - try { bootstrap.kill("SIGTERM"); } catch {} + } finally { + try { ws?.close(); } catch {} + try { if (execPid) process.kill(execPid, "SIGTERM"); } catch {} + await sleep(3000); + try { if (execPid) process.kill(execPid, "SIGKILL"); } catch {} + try { if (proc?.pid) process.kill(proc.pid, "SIGKILL"); } catch {} + try { bootstrap?.kill("SIGTERM"); } catch {} + } }Also applies to: 408-408
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/profiling/leak-investigation.mjs` around lines 139 - 406, The main() function currently can exit early and leave subprocesses (bootstrap, proc/executor) and the WebSocket (ws) open; wrap the runtime/measurement logic in a try { ... } finally { ... } block (or add an outer try/finally) that always runs cleanup: close ws if open, gracefully kill execPid and proc.pid with SIGTERM then SIGKILL fallback, and kill bootstrap if set; reference the variables/bootstrap creation (bootstrap), executor process (proc and execPid), and ws so the finally block reliably checks for existence and handles errors silently to guarantee teardown.rust-executor/src/wasm_core/README.md-91-103 (1)
91-103:⚠️ Potential issue | 🟠 MajorHost import module name is incorrect in docs.
Line 91 says imports come from
"ad4m", but runtime wiring expects"env"(HOST_MODULE_NAME). This mismatch will cause unresolved imports for language authors following this README.📝 Suggested doc fix
-### Host Functions (imports from "ad4m" module) +### Host Functions (imports from "env" module)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@rust-executor/src/wasm_core/README.md` around lines 91 - 103, The README lists host imports as coming from "ad4m" but the runtime uses HOST_MODULE_NAME ("env"), causing a mismatch; update the Host Functions table and any references that say `"ad4m"` to instead say `"env"` (or reference HOST_MODULE_NAME) so language authors see the correct module name for imports (ensure entries like `agent_did`, `agent_sign`, `log_message`, etc. are annotated as imports from `"env"`/HOST_MODULE_NAME).tests/js/wasm-integration-test.mjs-101-115 (1)
101-115:⚠️ Potential issue | 🟠 MajorGuarantee subprocess cleanup on all exit paths.
After startup (Line 101 onward), any uncaught error before Line 220 leaves
child/bootstrapalive because Line 226 exits immediately. Wrap runtime logic intry/finallyand terminate both processes infinally.💡 Suggested structure
async function main() { + let child; + let bootstrap; + let exitCode = 1; + try { // Bootstrap - const bootstrap = execCb(`${HOME}/.cargo/bin/kitsune2-bootstrap-srv`, { maxBuffer: 10*1024*1024 }); + bootstrap = execCb(`${HOME}/.cargo/bin/kitsune2-bootstrap-srv`, { maxBuffer: 10*1024*1024 }); // Start executor - const child = execCb(cmd, { env: { ...process.env, RUST_LOG: "info" } }); + child = execCb(cmd, { env: { ...process.env, RUST_LOG: "info" } }); // ...tests... - child.kill("SIGTERM"); - bootstrap.kill(); - await sleep(2000); - process.exit(failed > 0 ? 1 : 0); + exitCode = failed > 0 ? 1 : 0; + } finally { + try { child?.kill("SIGTERM"); } catch {} + try { bootstrap?.kill("SIGTERM"); } catch {} + await sleep(2000); + } + return exitCode; } -main().catch(e => { console.error("FATAL:", e); process.exit(1); }); +main() + .then(code => process.exit(code)) + .catch(e => { console.error("FATAL:", e); process.exit(1); });Also applies to: 220-223, 226-226
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/js/wasm-integration-test.mjs` around lines 101 - 115, The startup sequence spawns subprocesses (child from execCb and bootstrap) but doesn’t guarantee they are terminated on every exit path; wrap the runtime logic that starts and waits for the executor (the block that calls execCb, sets up child.stdout/stderr listeners, calls waitForServer, and later exits) in a try/finally so that in the finally you always attempt to clean up both child and bootstrap processes (check they are defined and alive, call child.kill("SIGTERM")/bootstrap.kill("SIGTERM") and fallback to SIGKILL if needed), and move any early exits (process.exit) to occur after cleanup or rethrow after cleanup; reference the child variable, bootstrap variable, waitForServer function and EXEC_LOG handling so logs are preserved before termination.rust-executor/src/wasm_core/mod.rs-1337-1344 (1)
1337-1344:⚠️ Potential issue | 🟠 MajorRe-registering the same address drops old instance without teardown.
Line 1341 overwrites existing entries via
insert; the replaced instance is dropped without invoking guest teardown, leaking runtime resources.💡 Suggested fix
- registry.insert( + if let Some(old) = registry.insert( language_address.to_string(), Arc::new(Mutex::new(instance)), - ); + ) { + if let Ok(mut old_instance) = old.lock() { + let _ = old_instance.teardown(); + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@rust-executor/src/wasm_core/mod.rs` around lines 1337 - 1344, Before inserting the new instance into WASM_LANGUAGE_REGISTRY, check for an existing entry for language_address and, if present, remove it and invoke the existing instance's guest teardown/cleanup while holding its Mutex (i.e., take the Arc<Mutex<...>> from the registry, lock it and call its teardown/cleanup method before dropping); then insert the newly created instance returned by load_wasm_language. Ensure you handle the registry lock poisoning errors the same way they are handled now, and log or propagate any errors from the teardown call so runtime resources are not leaked when replacing an entry inserted via registry.insert.rust-executor/src/wasm_core/mod.rs-1372-1384 (1)
1372-1384:⚠️ Potential issue | 🟠 MajorAvoid holding registry lock while running teardown.
unregister_wasm_languagekeeps the global registry mutex while calling guest teardown (Line 1380). Teardown may block or re-enter other runtime paths, increasing deadlock/latency risk. Remove entry first, drop lock, then teardown.💡 Suggested lock scope change
- let mut registry = WASM_LANGUAGE_REGISTRY - .lock() - .map_err(|e| WasmLanguageError::RuntimeError(format!("Registry lock poisoned: {}", e)))?; - if let Some(instance_arc) = registry.remove(language_address) { + let instance_arc = { + let mut registry = WASM_LANGUAGE_REGISTRY + .lock() + .map_err(|e| WasmLanguageError::RuntimeError(format!("Registry lock poisoned: {}", e)))?; + registry.remove(language_address) + }; + if let Some(instance_arc) = instance_arc { let mut instance = instance_arc .lock() .map_err(|e| WasmLanguageError::RuntimeError(format!("Instance lock poisoned: {}", e)))?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@rust-executor/src/wasm_core/mod.rs` around lines 1372 - 1384, unregister_wasm_language currently holds the global WASM_LANGUAGE_REGISTRY mutex while calling instance.teardown, which can deadlock; change the lock scope so you acquire the registry lock only to call registry.remove(language_address) (returning the Arc), then drop the registry lock before locking the removed instance and calling instance.teardown(); specifically: in the function that uses WASM_LANGUAGE_REGISTRY and registry.remove, make sure you own the returned instance_arc outside the registry lock, then lock instance_arc and call instance.teardown() (handling lock poison errors as before) and only afterward perform logging such as info!("Unregistered WASM language: {}", language_address).docs/profiling/publish-langs.mjs-50-189 (1)
50-189:⚠️ Potential issue | 🟠 MajorGuarantee child-process cleanup with
try/finally.Cleanup currently runs only on the success path (Lines 180-186). If any earlier step throws, the executor/bootstrap child processes can remain running.
🔧 Proposed fix
async function main() { + let bootstrap; + let proc; + let ws; writeFileSync(LOG, ""); log("=== Publishing bootstrap languages ==="); + try { // Start kitsune2-bootstrap-srv log("Starting bootstrap service..."); - const bootstrap = execCb("bash -lc 'kitsune2-bootstrap-srv'", { maxBuffer: 10*1024*1024 }); + bootstrap = execCb("bash -lc 'kitsune2-bootstrap-srv'", { maxBuffer: 10*1024*1024 }); @@ - const proc = execCb(cmd, { maxBuffer: 200*1024*1024, cwd: path.join(AD4M_DIR, "tests/js") }); + proc = execCb(cmd, { maxBuffer: 200*1024*1024, cwd: path.join(AD4M_DIR, "tests/js") }); @@ - const ws = new WebSocket(`ws://127.0.0.1:${PORT}/graphql`, "graphql-transport-ws"); + ws = new WebSocket(`ws://127.0.0.1:${PORT}/graphql`, "graphql-transport-ws"); @@ - // Cleanup - ws.close(); - try { process.kill(proc.pid, "SIGTERM"); } catch {} - try { bootstrap.kill("SIGTERM"); } catch {} - await sleep(2000); - try { process.kill(proc.pid, "SIGKILL"); } catch {} - try { bootstrap.kill("SIGKILL"); } catch {} - log("=== DONE ==="); + } finally { + try { ws?.close(); } catch {} + try { if (proc?.pid) process.kill(proc.pid, "SIGTERM"); } catch {} + try { bootstrap?.kill("SIGTERM"); } catch {} + await sleep(2000); + try { if (proc?.pid) process.kill(proc.pid, "SIGKILL"); } catch {} + try { bootstrap?.kill("SIGKILL"); } catch {} + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/profiling/publish-langs.mjs` around lines 50 - 189, The child processes and websocket must be torn down even on errors: wrap the main runtime section (after starting bootstrap via the bootstrap variable and starting executor into proc and creating ws) in a try { ... } finally { ... } block so cleanup always runs; move the existing cleanup logic (ws.close(), process.kill(proc.pid, ...), bootstrap.kill(...), fallback SIGKILL attempts and any sleep() waits) into the finally, guard against null/undefined (check proc and bootstrap and ws exist and have pid/kill/close methods) and keep any existing logging; ensure any outstanding promises/timeouts are allowed to settle or are cleared as needed before exiting the finally.wasm-language-sdk/src/memory.rs-59-64 (1)
59-64:⚠️ Potential issue | 🟠 MajorValidate fat-pointer bounds before
from_raw_parts.Line 63 dereferences host-provided
(ptr, len)without overflow or bounds checks. A malformed fat pointer can cause invalid memory reads. The host-supplied parameters are untrusted input at the WASM-host communication boundary.🔧 Proposed fix
pub fn read_input(ptr: u32, len: u32) -> Vec<u8> { if ptr == 0 || len == 0 { return Vec::new(); } + let end = match ptr.checked_add(len) { + Some(v) => v, + None => return Vec::new(), + }; + #[cfg(target_arch = "wasm32")] + { + let mem_size_bytes = (unsafe { core::arch::wasm32::memory_size(0) } as u64) * 65536; + if (end as u64) > mem_size_bytes { + return Vec::new(); + } + } let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, len as usize) }; slice.to_vec() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wasm-language-sdk/src/memory.rs` around lines 59 - 64, read_input currently calls std::slice::from_raw_parts with untrusted ptr and len; validate both for integer overflow and that the byte range fits inside the module's linear memory before creating the slice. Perform a checked_add on ptr as usize and len as usize (returning an empty Vec on overflow or zero), then compare the end offset against the current wasm linear memory size in bytes (e.g., using core::arch::wasm32::memory_size(0) * 65536 when targeting wasm32) and only call from_raw_parts when the range is fully inside bounds; otherwise return an empty Vec. Ensure you reference read_input, ptr, len and the unsafe from_raw_parts call when applying the checks.examples/wasm-languages/p-diff-sync-wasm/src/lib.rs-233-243 (1)
233-243:⚠️ Potential issue | 🟠 MajorMake
init()idempotent
init()always installs a new app. If initialization is called more than once in a module lifetime, this can create duplicate installs and overwriteapp_id.Suggested fix
impl LanguageInit for PDiffSyncLanguage { fn init(&mut self) -> Result<(), String> { + if self.installed { + log("p-diff-sync-wasm: init() skipped, already installed"); + return Ok(()); + } log("p-diff-sync-wasm: init() - installing DNA..."); log(&format!("p-diff-sync-wasm: .happ bundle size: {} bytes", HAPP_BYTES.len()));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/wasm-languages/p-diff-sync-wasm/src/lib.rs` around lines 233 - 243, init() currently always calls holochain_install_app(HAPP_BYTES) and overwrites self.app_id; make init idempotent by first checking the instance state and skipping installation when already initialized: return Ok(()) immediately if self.installed is true or self.app_id is Some(...). If not initialized, proceed to call holochain_install_app(HAPP_BYTES), set self.app_id from the returned info and set self.installed = true; ensure any error paths leave state unchanged so subsequent calls can retry.rust-executor/src/perspectives/perspective_instance.rs-275-288 (1)
275-288:⚠️ Potential issue | 🟠 MajorDon’t drop
link_languagereference after failedlanguage_removeAt Line 275 failure is logged, but Line 288 still clears
self.link_language. If removal fails, this loses the in-memory handle and prevents retry/inspection while resources may still be alive.Suggested fix
- if let Some(ref nh) = handle.neighbourhood { + let mut remove_ok = true; + if let Some(ref nh) = handle.neighbourhood { let link_language_address = nh.data.link_language.clone(); log::info!("🧹 Perspective {} is a neighbourhood, removing link language: {}", uuid, link_language_address); if let Err(e) = LanguageController::language_remove(link_language_address.clone()).await { log::error!("Error unloading link language {} for perspective {}: {:?}", link_language_address, uuid, e); + remove_ok = false; } } @@ - *self.link_language.write().await = None; + if remove_ok { + *self.link_language.write().await = None; + } else { + log::warn!("Keeping link_language reference for perspective {} after failed language_remove", uuid); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@rust-executor/src/perspectives/perspective_instance.rs` around lines 275 - 288, The code currently logs errors from LanguageController::language_remove(...) but always clears the in-memory handle via *self.link_language.write().await = None; which drops the handle on failure; change the flow so that the link language reference is only cleared when LanguageController::language_remove(...) returns Ok: move the write-to-None of self.link_language (and any cleanup that assumes successful unload) into the success branch (the Ok path) and skip clearing the handle when the call returns Err so the in-memory reference remains available for retry/inspection; locate LanguageController::language_remove, the error logging block, and the assignment to self.link_language.write().await to implement this conditional behavior.examples/wasm-languages/p-diff-sync-wasm/src/lib.rs-221-228 (1)
221-228:⚠️ Potential issue | 🟠 MajorDon’t mark the DNA as removed when uninstall fails
At Line 225, uninstall errors are ignored, but Line 227 still flips
installedtofalse. That can orphan an installed app and make follow-up cleanup impossible from this instance.Suggested fix
impl LanguageTeardown for PDiffSyncLanguage { fn teardown(&mut self) { log("p-diff-sync-wasm: teardown"); if self.installed { if let Some(ref app_id) = self.app_id { - let _ = holochain_remove_app(app_id); + match holochain_remove_app(app_id) { + Ok(_) => { + self.installed = false; + self.app_id = None; + } + Err(e) => { + log(&format!("p-diff-sync-wasm: failed to remove app {}: {}", app_id, e)); + } + } + } else { + // Keep state explicit; no app_id means we can't safely uninstall. + log("p-diff-sync-wasm: teardown skipped, missing app_id"); } - self.installed = false; } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/wasm-languages/p-diff-sync-wasm/src/lib.rs` around lines 221 - 228, In teardown(), don’t unconditionally flip the instance’s installed flag to false when holochain_remove_app fails; instead call holochain_remove_app(app_id), check its Result/Option outcome and only set self.installed = false on successful uninstall, logging or returning the error when it fails so the instance remains marked installed; update the teardown (and any helpers) to handle the error path for holochain_remove_app and preserve self.installed when removal fails, referencing the teardown method, the installed field, app_id, and holochain_remove_app call.wasm-language-sdk/src/lib.rs-190-194 (1)
190-194:⚠️ Potential issue | 🟠 MajorAvoid creating a fresh language instance during teardown
Line 192 calls
get_language(), which lazily constructs a default instance during teardown if none exists. That can trigger unintended side effects and leaves stale state because the static instance is never cleared.Suggested fix
#[no_mangle] pub extern "C" fn ad4m_teardown() { - let lang = get_language(); - lang.teardown(); + unsafe { + if let Some(mut lang) = LANGUAGE_INSTANCE.take() { + lang.teardown(); + } + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wasm-language-sdk/src/lib.rs` around lines 190 - 194, The ad4m_teardown function currently calls get_language(), which can lazily construct a new default Language during teardown; instead, change ad4m_teardown to look up the existing static Language instance without creating one (e.g., use the static's Option/get_if_initialized accessor or add a get_language_optional helper) and only call teardown on it if present, then clear/set the static to None so no stale instance remains; reference get_language and ad4m_teardown when making this change.wasm-language-sdk/src/host.rs-42-44 (1)
42-44:⚠️ Potential issue | 🟠 MajorDon’t treat zero-length host output as null
Line 42 rejects
len == 0, which can incorrectly convert valid empty binary responses into errors.Suggested fix
fn read_host_result(fat_ptr: u64) -> Option<Vec<u8>> { if fat_ptr == 0 { return None; } let (ptr, len) = decode_fat_ptr(fat_ptr); - if ptr == 0 || len == 0 { + if ptr == 0 { return None; } Some(read_input(ptr, len)) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wasm-language-sdk/src/host.rs` around lines 42 - 44, The code currently treats len == 0 as a null response; change the null-check in host.rs to only consider ptr == 0 as null (i.e., replace "if ptr == 0 || len == 0 { return None; }" with a check that returns None only when ptr == 0), and ensure the subsequent logic that reads the host memory (the code that uses ptr and len to build the response buffer) correctly handles len == 0 by returning an empty byte/vector rather than None.wasm-language-sdk/src/host.rs-155-158 (1)
155-158:⚠️ Potential issue | 🟠 MajorValidate numeric byte ranges before converting to
u8Lines 157 and 240 use
n as u8, which silently truncates out-of-range values (e.g., 256 → 0, 257 → 1). This can corrupt returned binary data, particularly problematic inholochain_get_agent_key()which parses cryptographic key material.Use
u8::try_from(n)to validate that values are in the 0–255 range and return an error if the host provides malformed data.Suggested fix pattern
- return Ok(arr.iter().filter_map(|v| v.as_u64().map(|n| n as u8)).collect()); + let mut out = Vec::with_capacity(arr.len()); + for v in arr { + let n = v + .as_u64() + .ok_or_else(|| "non-numeric byte in host response".to_string())?; + let b = u8::try_from(n).map_err(|_| format!("byte out of range: {n}"))?; + out.push(b); + } + return Ok(out);Also applies to: 238–241
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@wasm-language-sdk/src/host.rs` around lines 155 - 158, The code silently truncates out-of-range integers when converting host JSON array items to bytes (using `n as u8`), which can corrupt binary data; update the conversion in the `if let Some(ok_data) = val.get("Ok") { ... }` block and in the `holochain_get_agent_key()` parsing path to use `u8::try_from(n)` (or `TryFrom::<u64>::try_from(n)`) and propagate a clear error if the conversion fails so invalid values outside 0..=255 are rejected instead of truncated.rust-executor/src/languages/language.rs-264-352 (1)
264-352:⚠️ Potential issue | 🟠 MajorReplace
lock().unwrap()with error propagationLines 264, 272, 310, 318, and 347 use
lock().unwrap()on aMutex. A poisoned mutex will panic and crash the backend instead of returning a controlled error.Extract a helper method to map poisoned mutexes to
AnyError:Suggested fix
impl WasmLanguage { pub fn new(instance: Arc<Mutex<WasmLanguageInstance>>) -> Self { Self { instance } } + + fn lock_instance(&self) -> Result<std::sync::MutexGuard<'_, WasmLanguageInstance>, AnyError> { + self.instance + .lock() + .map_err(|e| anyhow::anyhow!("WasmLanguageInstance mutex poisoned: {e}")) + } } @@ - let mut instance = self.instance.lock().unwrap(); + let mut instance = self.lock_instance()?;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@rust-executor/src/languages/language.rs` around lines 264 - 352, The code currently uses instance.lock().unwrap() in methods sync, commit, current_revision, render, and others which will panic on a poisoned Mutex; add a helper method (e.g., lock_instance or get_instance_lock) that takes &self, calls self.instance.lock(), maps a PoisonError into an AnyError (using anyhow::anyhow! with a clear message) and returns Result<MutexGuard<...>, AnyError>, then replace every .lock().unwrap() call in the functions sync, commit, current_revision, render, and others with this helper and propagate the error instead of unwrapping so a poisoned mutex returns a controlled AnyError.
🟡 Minor comments (6)
rust-executor/src/surreal_service/mod.rs-736-745 (1)
736-745:⚠️ Potential issue | 🟡 MinorUpdate the shutdown() comment to reflect actual RocksDB usage, not in-memory storage.
The comment at line 740 claims "SurrealDB uses in-memory storage (Surreal::new::)", but in production the code uses RocksDB via
Surreal::new::<RocksDb>(line 333 innew()) whendata_pathis provided. In-memory storage is only a fallback whendata_pathisNone(tests only). Update the comment to accurately reflect the RocksDB-based persistence in production.The current
&selfsignature is correct given the architecture:SurrealDBServiceis Arc-wrapped and owned byPerspectiveInstance, so the Arc is dropped when the instance is removed from thePERSPECTIVESmap inremove_perspective(), not byshutdown()itself. The method's no-op implementation is intentional and acceptable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@rust-executor/src/surreal_service/mod.rs` around lines 736 - 745, Update the docstring for shutdown() to correctly describe storage: replace the claim that SurrealDB uses in-memory storage (Surreal::new::<Mem>) with a note that the production service uses RocksDB via Surreal::new::<RocksDb> when a data_path is provided (in-memory is only the test fallback); keep the rest of the comment explaining that shutdown() is a no-op because the Arc-wrapped SurrealDBService is dropped when the perspective is removed from PERSPECTIVES (see remove_perspective()) and that the no-op signature pub async fn shutdown(&self) -> Result<(), Error> is intentional.docs/profiling/profiler-v9.mjs-107-108 (1)
107-108:⚠️ Potential issue | 🟡 MinorAvoid logging the raw admin credential in command output.
Line 108 logs
--admin-credentialinto persistent logs. Please redact it before writing logs.🔒 Minimal redaction fix
- log(`CMD: ${cmd}`); + log(`CMD: ${cmd.replace(/--admin-credential\s+\S+/, "--admin-credential [REDACTED]")}`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/profiling/profiler-v9.mjs` around lines 107 - 108, The log currently prints the full command string stored in cmd which includes the sensitive admin credential (TOKEN); before calling log(`CMD: ${cmd}`) construct a redacted version (e.g., replace or omit the --admin-credential ${TOKEN} segment) and log that instead so the real TOKEN is never written to persistent logs; locate the code that builds cmd (uses EXECUTOR, DATA, PORT, bootstrapUrl, TOKEN) and emit a sanitizedCmd for logging while preserving the original cmd for execution only.docs/profiling/leak-investigation.mjs-145-147 (1)
145-147:⚠️ Potential issue | 🟡 MinorFail fast when the seed does not provide a link language hash.
Later mutations depend on this value; without a guard, failures surface much later and are hard to diagnose.
✅ Precondition guard
const seedData = JSON.parse(readFileSync(SEED, "utf-8")); const linkLangAddr = seedData.knownLinkLanguages?.[0]; log(`Link language (p-diff-sync): ${linkLangAddr}`); + if (!linkLangAddr) { + throw new Error(`Missing knownLinkLanguages[0] in seed file: ${SEED}`); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/profiling/leak-investigation.mjs` around lines 145 - 147, The code reads seedData and assigns linkLangAddr from seedData.knownLinkLanguages?.[0] but doesn't guard against a missing value; add a precondition check after computing linkLangAddr that verifies it is defined (and non-empty) and immediately fails with a clear error including the SEED path or relevant context (throw an Error or call process.exit(1) after logging) so downstream mutations won't run with an undefined linkLangAddr; reference the variables seedData, linkLangAddr, SEED and the existing readFileSync/log usage when adding the guard.docs/profiling/refactoring-plan.md-3-5 (1)
3-5:⚠️ Potential issue | 🟡 MinorDocument date appears stale relative to this profiling batch.
Line 3 says
2025-02-21, while the surrounding profiling artifacts in this PR are dated 2026. Please confirm the intended date to avoid timeline confusion.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/profiling/refactoring-plan.md` around lines 3 - 5, The document header contains a stale date string "Date: 2025-02-21" which conflicts with the 2026-dated profiling artifacts; update the Date value in the header to the correct profiling batch date (e.g., 2026-XX-XX) or replace it with a clear marker like "Date: 2026-02-XX" after confirming the intended date with the author (refer to the header line containing "Date:" in docs/profiling/refactoring-plan.md).docs/profiling/profiling-results-2026-02-21.md-31-38 (1)
31-38:⚠️ Potential issue | 🟡 MinorAdd blank lines around tables to satisfy markdownlint (MD058).
At Line 31, Line 40, Line 49, and Line 58, the tables are not surrounded by blank lines. This will keep lint warnings active in docs checks.
Also applies to: 40-47, 49-56, 58-65
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/profiling/profiling-results-2026-02-21.md` around lines 31 - 38, The markdown tables (starting with the header line "| Category | MB | % |") need blank lines before and after each table block to satisfy markdownlint rule MD058; edit the document profiling-results-2026-02-21.md and insert an empty line immediately above and immediately below each table occurrence (the four tables whose headers are "| Category | MB | % |" and the subsequent rows like "Anonymous mappings | 244.1 | 68%") so every table is separated from surrounding text by blank lines.docs/profiling/refactoring-plan.md-25-32 (1)
25-32:⚠️ Potential issue | 🟡 MinorSpecify languages for fenced code blocks.
At Line 25 and Line 199, fenced blocks omit a language identifier, which triggers MD040 and reduces readability in rendered docs.
Also applies to: 199-215
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/profiling/refactoring-plan.md` around lines 25 - 32, The fenced code blocks that show the stack trace for "GraphQL perspectiveRemove" and the later block around "instance.teardown_background_tasks()" lack language identifiers; update those fenced blocks to include an appropriate language tag (e.g., ```text or ```rust) so the markdown linter (MD040) is satisfied and rendered output is clearer. Locate the two blocks that contain the stack trace lines referencing GraphQL perspectiveRemove → perspectives::remove_perspective(uuid) → instance.teardown_background_tasks() and add the same consistent language identifier to both fenced-code openings.
🧹 Nitpick comments (2)
rust-executor/src/wasm_core/tests.rs (1)
25-31: Avoid silent pass-on-missing-fixture behavior in runtime tests.These early returns make CI pass with reduced coverage when fixtures are absent. Prefer explicit skip semantics or fail in CI (
CI=true) so regressions are not hidden.Based on learnings: "When Rust code in `rust-executor/` changes, run `cargo build --release` in the `cli/` directory before running integration tests" and "When JS code in `executor/` changes, run `pnpm build` in both `executor/` and `rust-executor/` directories before running integration tests".💡 Suggested helper pattern
+fn fixture_available(path: &std::path::Path) -> bool { + if path.exists() { + return true; + } + if std::env::var("CI").is_ok() { + panic!("Missing WASM fixture in CI: {}", path.display()); + } + eprintln!("Skipping test: missing fixture {}", path.display()); + false +}Also applies to: 42-44, 58-60, 70-72, 82-84, 94-96, 105-107, 124-126, 170-171, 181-182, 190-191, 199-200, 240-241, 291-292, 312-315, 340-341
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@rust-executor/src/wasm_core/tests.rs` around lines 25 - 31, The current early-return when a WASM fixture is missing (the wasm_path.exists() check) silently skips tests; change it to explicitly fail in CI by checking the CI environment variable and assert/panic when CI is true (e.g., if std::env::var("CI") == Ok("true".into()) or "1"), otherwise keep the current skip message and return; update the block around wasm_path.exists() to perform this env check and call panic!/assert! with the same descriptive message when CI is set, referencing wasm_path and its .exists() check so CI runs fail instead of silently passing.rust-executor/src/wasm_core/README.md (1)
91-103: Host function list is missing newly supported Holochain imports.Please add
hc_install_app,hc_remove_app, andhc_get_agent_keyto keep README and runtime ABI aligned.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@rust-executor/src/wasm_core/README.md` around lines 91 - 103, The Host Functions table under "imports from \"ad4m\" module" is missing the new Holochain imports; add rows for hc_install_app, hc_remove_app, and hc_get_agent_key to that table (place them alongside the existing hc_call entry) and include their correct signatures and short descriptions matching the runtime ABI (use the same u32/u64 return patterns as other hc_* functions in the file and mirror the style of descriptions like "Install a Holochain app", "Remove a Holochain app", and "Get agent public key"). Ensure the three symbols hc_install_app, hc_remove_app, and hc_get_agent_key are spelled exactly as shown so the README aligns with the runtime ABI.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (6)
examples/wasm-languages/link-store/Cargo.lockis excluded by!**/*.lockexamples/wasm-languages/note-store/Cargo.lockis excluded by!**/*.lockexamples/wasm-languages/p-diff-sync-wasm/Cargo.lockis excluded by!**/*.lockrust-executor/tests/fixtures/wasm/link_store_wasm.wasmis excluded by!**/*.wasmrust-executor/tests/fixtures/wasm/note_store_wasm.wasmis excluded by!**/*.wasmwasm-language-sdk/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (41)
.github/workflows/exploration-ci.ymlCargo.tomlcli/Cargo.tomldocs/profiling/README.mddocs/profiling/leak-investigation-2026-02-21.mddocs/profiling/leak-investigation.mjsdocs/profiling/profiler-v9.mjsdocs/profiling/profiling-results-2026-02-21.mddocs/profiling/publish-langs.mjsdocs/profiling/refactoring-plan.mdexamples/wasm-languages/link-store/Cargo.tomlexamples/wasm-languages/link-store/src/lib.rsexamples/wasm-languages/note-store/Cargo.tomlexamples/wasm-languages/note-store/src/lib.rsexamples/wasm-languages/p-diff-sync-wasm/Cargo.tomlexamples/wasm-languages/p-diff-sync-wasm/src/lib.rsrust-client/schema.gqlrust-executor/Cargo.tomlrust-executor/build.rsrust-executor/src/graphql/mutation_resolvers.rsrust-executor/src/holochain_service/mod.rsrust-executor/src/js_core/languages_extension.jsrust-executor/src/js_core/languages_extension.rsrust-executor/src/languages/language.rsrust-executor/src/languages/mod.rsrust-executor/src/lib.rsrust-executor/src/perspectives/mod.rsrust-executor/src/perspectives/perspective_instance.rsrust-executor/src/prolog_service/mod.rsrust-executor/src/surreal_service/mod.rsrust-executor/src/wasm_core/README.mdrust-executor/src/wasm_core/abi.rsrust-executor/src/wasm_core/error.rsrust-executor/src/wasm_core/mod.rsrust-executor/src/wasm_core/tests.rstests/js/wasm-integration-test.mjswasm-language-sdk/Cargo.tomlwasm-language-sdk/src/host.rswasm-language-sdk/src/lib.rswasm-language-sdk/src/memory.rswasm-language-sdk/src/types.rs
| default = ["surrealdb-links"] | ||
| metal = ["kalosm/metal"] | ||
| cuda = ["kalosm/cuda"] | ||
| generate_snapshot = [] # Feature flag for snapshot generation mode | ||
| wasm-languages = ["dep:wasmer"] | ||
| # Link storage backend selection (mutually exclusive) | ||
| surrealdb-links = ["dep:surrealdb"] | ||
| sqlite-links = ["dep:urlencoding"] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's read the Cargo.toml file to understand the full context
cat -n rust-executor/Cargo.toml | head -60Repository: coasys/ad4m
Length of output: 2749
🏁 Script executed:
#!/bin/bash
# Run the validation script provided in the review
set -euo pipefail
python - <<'PY'
import pathlib, tomllib, sys
cargo_toml = pathlib.Path("rust-executor/Cargo.toml")
data = tomllib.loads(cargo_toml.read_text())
deps = data.get("dependencies", {})
features = data.get("features", {})
errors = []
for feat, vals in features.items():
if not isinstance(vals, list):
continue
for v in vals:
if isinstance(v, str) and v.startswith("dep:"):
dep = v.split(":", 1)[1]
if dep not in deps:
errors.append(f"feature '{feat}' references missing dependency '{dep}'")
continue
dep_def = deps[dep]
optional = isinstance(dep_def, dict) and dep_def.get("optional") is True
if not optional:
errors.append(f"feature '{feat}' uses dep:{dep} but dependency is not optional")
if errors:
print("Manifest validation failed:")
for e in errors:
print("-", e)
sys.exit(1)
print("Manifest validation passed: all dep:* feature targets are optional dependencies.")
PYRepository: coasys/ad4m
Length of output: 303
🏁 Script executed:
#!/bin/bash
# Check for surrealdb in the dependencies
grep -n "surrealdb" rust-executor/Cargo.tomlRepository: coasys/ad4m
Length of output: 316
🏁 Script executed:
#!/bin/bash
# Get a better view of all dependencies to see surrealdb
cat rust-executor/Cargo.toml | grep -A 100 "\[dependencies\]" | head -80Repository: coasys/ad4m
Length of output: 3668
Feature-to-dependency wiring is currently inconsistent and can break Cargo feature resolution.
Line 35 references dep:wasmer, but no optional wasmer dependency is declared in this manifest.
Lines 37-38 reference dep:surrealdb and dep:urlencoding, while surrealdb (line 146) and urlencoding (line 54) are currently non-optional dependencies. This prevents these features from behaving as valid dependency gates.
🔧 Suggested manifest fix
[dependencies]
-urlencoding = "2.1"
+urlencoding = { version = "2.1", optional = true }
...
-surrealdb = { version = "2.4", default-features = false, features = ["kv-rocksdb", "kv-mem", "scripting"] }
+surrealdb = { version = "2.4", default-features = false, features = ["kv-rocksdb", "kv-mem", "scripting"], optional = true }
+# Add an optional `wasmer` dependency pinned to the project's intended version.
+# wasmer = { version = "<pin>", optional = true }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@rust-executor/Cargo.toml` around lines 31 - 38, The Cargo features currently
reference dependencies as feature gates but those crates aren't declared as
optional; make the referenced crates optional and/or correct the referenced
crate names: add an optional dependency entry for wasmer (e.g., declare the
wasmer crate under [dependencies] with optional = true) or change the feature to
reference the actual crate name, and mark surrealdb and urlencoding dependencies
as optional (set optional = true in their [dependencies] entries) so the
features surrealdb-links and sqlite-links can act as dependency gates (features:
wasm-languages -> dep:wasmer, surrealdb-links -> dep:surrealdb, sqlite-links ->
dep:urlencoding).
| crate::languages::LanguageController::install_wasm_language( | ||
| std::path::Path::new(&wasm_path), | ||
| &address, | ||
| ).map_err(|e| FieldError::new( | ||
| format!("WASM language install error: {}", e), | ||
| coasys_juniper::Value::null(), | ||
| ))?; | ||
| return Ok(address); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for install_wasm_language calls and definitions
echo "=== Searching for install_wasm_language ==="
rg -n 'install_wasm_language' rust-executor/src/ || echo "No matches found"
# Search for the proposed fix method
echo -e "\n=== Searching for register_wasm_language ==="
rg -n 'register_wasm_language' rust-executor/src/ || echo "No matches found"
# Look at LanguageController structure
echo -e "\n=== Searching for LanguageController impl ==="
rg -n 'impl LanguageController' rust-executor/src/ || echo "No impl found"
# Check if there's a languages module with relevant functions
echo -e "\n=== Functions in languages module ==="
rg -n '^\s*pub fn\s+\w+.*language' rust-executor/src/languages/ --max-count=20 || echo "None found"Repository: coasys/ad4m
Length of output: 1648
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the LanguageController impl block structure
echo "=== LanguageController impl around line 905 ==="
sed -n '100,110p' rust-executor/src/languages/mod.rs
echo ""
echo "=== Context around line 905 (install_wasm_language call) ==="
sed -n '895,915p' rust-executor/src/languages/mod.rs
echo ""
echo "=== Search for install_wasm_language method definition in LanguageController ==="
rg -B5 -A10 'pub fn install_wasm_language' rust-executor/src/languages/mod.rs | head -30Repository: coasys/ad4m
Length of output: 1416
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for install_wasm_language without requiring 'pub'
echo "=== All install_wasm_language in languages/mod.rs ==="
rg -B3 -A8 'fn install_wasm_language' rust-executor/src/languages/mod.rs
# Check for feature-gating
echo -e "\n=== Check feature gates around install_wasm_language ==="
rg -B10 'fn install_wasm_language' rust-executor/src/languages/mod.rs | head -20
# Check the total line count to verify context extraction
echo -e "\n=== Total lines in languages/mod.rs ==="
wc -l rust-executor/src/languages/mod.rs
# Look for cfg attributes or feature gates
echo -e "\n=== Check for wasm-languages feature in languages/mod.rs ==="
rg '#\[cfg.*wasm' rust-executor/src/languages/mod.rs || echo "No wasm feature gates found"Repository: coasys/ad4m
Length of output: 108
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Get broader context around line 905 with method signatures
echo "=== Broader context around line 905 (last 50 lines before, 20 after) ==="
sed -n '855,925p' rust-executor/src/languages/mod.rs | cat -n
# Count and list methods in LanguageController impl
echo -e "\n=== Methods in LanguageController impl (first 30) ==="
rg '^\s+pub fn ' rust-executor/src/languages/mod.rs | head -30
# Check the actual mutation_resolvers.rs file at line 1603
echo -e "\n=== Context in mutation_resolvers.rs around line 1603 ==="
sed -n '1595,1615p' rust-executor/src/graphql/mutation_resolvers.rs | cat -nRepository: coasys/ad4m
Length of output: 4803
Use crate::wasm_core::register_wasm_language to register WASM languages.
Line 1603 calls crate::languages::LanguageController::install_wasm_language(...), which does not exist in the LanguageController implementation and will fail compilation under the wasm-languages feature. Additionally, the same non-existent method is called at line 905 in languages/mod.rs within install_wasm_from_base64.
🔧 Fix: Use the correct wasm_core API
Replace crate::languages::LanguageController::install_wasm_language with crate::wasm_core::register_wasm_language. This function exists at rust-executor/src/wasm_core/mod.rs:1333 and already handles WASM registration correctly, as demonstrated by its use in js_core/languages_extension.rs:88.
- crate::languages::LanguageController::install_wasm_language(
+ crate::wasm_core::register_wasm_language(
std::path::Path::new(&wasm_path),
&address,
).map_err(|e| FieldError::new(The same fix should be applied at line 905 in languages/mod.rs.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| crate::languages::LanguageController::install_wasm_language( | |
| std::path::Path::new(&wasm_path), | |
| &address, | |
| ).map_err(|e| FieldError::new( | |
| format!("WASM language install error: {}", e), | |
| coasys_juniper::Value::null(), | |
| ))?; | |
| return Ok(address); | |
| crate::wasm_core::register_wasm_language( | |
| std::path::Path::new(&wasm_path), | |
| &address, | |
| ).map_err(|e| FieldError::new( | |
| format!("WASM language install error: {}", e), | |
| coasys_juniper::Value::null(), | |
| ))?; | |
| return Ok(address); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@rust-executor/src/graphql/mutation_resolvers.rs` around lines 1603 - 1610,
The call to the non-existent
crate::languages::LanguageController::install_wasm_language should be replaced
with crate::wasm_core::register_wasm_language; update the invocation in
mutation_resolvers (the call around install_wasm_language(...) returning
address) and the analogous site inside install_wasm_from_base64 in
languages/mod.rs to call crate::wasm_core::register_wasm_language with the same
arguments (Path::new(&wasm_path) and &address) and preserve the existing error
mapping to FieldError::new; use the register_wasm_language symbol from wasm_core
instead of LanguageController::install_wasm_language.
| /// Get the languages directory path from JS core | ||
| pub fn languages_path() -> String { | ||
| let instance = Self::global_instance(); | ||
| format!("{}/ad4m/languages", instance.app_data_path) | ||
| } | ||
|
|
||
| /// Fetch language source from the language language via JS | ||
| async fn fetch_language_source(address: &str) -> Result<String, AnyError> { | ||
| Self::global_instance() | ||
| .js_core | ||
| .execute("await core.waitForLanguages()".into()) | ||
| .await?; | ||
|
|
||
| let script = format!( | ||
| r#"await core.languageController.getLanguageSource("{}")"#, | ||
| address, | ||
| ); | ||
| let result = Self::global_instance().js_core.execute(script).await?; | ||
| if result == "null" || result.is_empty() { | ||
| return Err(deno_core::anyhow::anyhow!("Language source not found: {}", address)); | ||
| } | ||
| Ok(result.trim_matches('"').to_string()) | ||
| } | ||
|
|
||
| /// Fetch language meta JSON from the language language via JS | ||
| async fn fetch_language_meta(address: &str) -> Result<String, AnyError> { | ||
| let script = format!( | ||
| r#"JSON.stringify(await core.languageController.getLanguageExpression("{}"))"#, | ||
| address, | ||
| ); | ||
| Self::global_instance().js_core.execute(script).await | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Struct definition (Lines 82-105) ==="
sed -n '82,105p' rust-executor/src/languages/mod.rs
echo ""
echo "=== Method implementations (Lines 840-875) ==="
sed -n '840,875p' rust-executor/src/languages/mod.rs
echo ""
echo "=== All occurrences of app_data_path and js_core in the file ==="
rg -nP '\b(app_data_path|js_core)\b' rust-executor/src/languages/mod.rsRepository: coasys/ad4m
Length of output: 2951
LanguageController struct does not have app_data_path or js_core fields.
Lines 846, 852, 860, 873, 936, 942, and 956 attempt to access app_data_path and js_core, but these fields do not exist in the struct definition (lines 82-105). This causes a compile-time error.
🔧 Suggested fix for languages_path()
pub fn languages_path() -> String {
- let instance = Self::global_instance();
- format!("{}/ad4m/languages", instance.app_data_path)
+ crate::utils::languages_directory()
+ .to_string_lossy()
+ .to_string()
}Also applies to: 852, 860, 873, 936, 942, 956 (all js_core field access issues)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@rust-executor/src/languages/mod.rs` around lines 843 - 874, The code is
trying to access non-existent fields app_data_path and js_core on the
LanguageController returned by Self::global_instance(); update the
LanguageController struct to include those fields (e.g., add pub app_data_path:
String and pub js_core: /* appropriate JS runtime type, e.g.,
deno_core::JsRuntime or a wrapper */) and ensure Self::global_instance()
initializes them, or alternatively replace those field accesses in
languages_path, fetch_language_source, and fetch_language_meta with the correct
accessor methods if the runtime and app-data are exposed differently;
specifically modify the struct definition used by global_instance and its
initializer so languages_path(), fetch_language_source(), and
fetch_language_meta() can call instance.app_data_path and
instance.js_core.execute(...) without compile errors.
| async fn install_wasm_from_base64(base64_data: &str, address: &str) -> Result<(), AnyError> { | ||
| use base64::Engine; | ||
|
|
||
| let wasm_bytes = base64::engine::general_purpose::STANDARD | ||
| .decode(base64_data) | ||
| .map_err(|e| deno_core::anyhow::anyhow!("Base64 decode error: {}", e))?; | ||
|
|
||
| // Verify WASM magic | ||
| if wasm_bytes.len() < 4 || &wasm_bytes[0..4] != b"\0asm" { | ||
| return Err(deno_core::anyhow::anyhow!("Decoded data is not valid WASM")); | ||
| } | ||
|
|
||
| // Save to languages directory | ||
| let languages_path = Self::languages_path(); | ||
| let lang_dir = format!("{}/{}", languages_path, address); | ||
| std::fs::create_dir_all(&lang_dir)?; | ||
| let bundle_path = format!("{}/bundle.wasm", lang_dir); | ||
| std::fs::write(&bundle_path, &wasm_bytes)?; | ||
| log::info!("Saved WASM bundle ({} bytes) to {}", wasm_bytes.len(), bundle_path); | ||
|
|
||
| // Register in WASM runtime | ||
| Self::install_wasm_language(std::path::Path::new(&bundle_path), address)?; | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# First, let's check the file structure and find install_wasm_language references
echo "=== Searching for install_wasm_language in the file ==="
rg -nP 'install_wasm_language' rust-executor/src/languages/mod.rs
echo ""
echo "=== Checking file size ==="
wc -l rust-executor/src/languages/mod.rsRepository: coasys/ad4m
Length of output: 265
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check if wasm_core module exists and has register_wasm_language
echo "=== Searching for register_wasm_language in wasm_core ==="
fd -t f 'wasm_core' rust-executor/src/ && echo "Found wasm_core"
rg -l 'register_wasm_language' rust-executor/src/
echo ""
echo "=== Looking for wasm_core module definition ==="
rg 'mod wasm_core|pub mod wasm_core' rust-executor/src/Repository: coasys/ad4m
Length of output: 342
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Let's look at the specific lines around 884-907 to see the context
echo "=== Reading lines 880-910 from the file ==="
sed -n '880,910p' rust-executor/src/languages/mod.rs | cat -nRepository: coasys/ad4m
Length of output: 1611
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Let's find the impl block structure and all methods
echo "=== Finding the impl block containing install_wasm_from_base64 ==="
rg -B 20 'async fn install_wasm_from_base64' rust-executor/src/languages/mod.rs | head -30
echo ""
echo "=== Searching for all fn definitions in the impl block to find install_wasm_language ==="
rg 'fn install_wasm_language' rust-executor/src/languages/mod.rsRepository: coasys/ad4m
Length of output: 1048
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Let's check the wasm_core module for register_wasm_language
echo "=== Looking at wasm_core/mod.rs for register_wasm_language ==="
rg -n 'fn register_wasm_language|pub fn register_wasm_language' rust-executor/src/wasm_core/mod.rsRepository: coasys/ad4m
Length of output: 153
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Let's get the full signature of register_wasm_language in wasm_core
echo "=== Getting context around register_wasm_language ==="
rg -B 2 -A 5 'pub.*fn register_wasm_language' rust-executor/src/wasm_core/mod.rs | head -20Repository: coasys/ad4m
Length of output: 407
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Let's check the impl block structure - find where it starts
echo "=== Finding impl block start ==="
rg -n 'impl LanguageController' rust-executor/src/languages/mod.rs
echo ""
echo "=== Verify install_wasm_language does not exist anywhere in the file ==="
rg -c 'fn install_wasm_language' rust-executor/src/languages/mod.rs || echo "Method not found (exit code $?)"
echo ""
echo "=== Check all methods in the impl block by looking for 'fn ' pattern ==="
rg -n '^\s+(?:async\s+)?fn\s+' rust-executor/src/languages/mod.rs | grep -E '(install_|wasm)' | head -20Repository: coasys/ad4m
Length of output: 494
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check error type compatibility - see if WasmLanguageError can be converted to AnyError
echo "=== Checking WasmLanguageError definition and error trait implementations ==="
rg -B 2 -A 10 'enum WasmLanguageError|struct WasmLanguageError' rust-executor/src/wasm_core/mod.rs | head -20
echo ""
echo "=== Checking if WasmLanguageError implements From trait or is compatible with AnyError ==="
rg 'impl.*From.*WasmLanguageError|impl.*From.*for WasmLanguageError' rust-executor/src/wasm_core/mod.rsRepository: coasys/ad4m
Length of output: 132
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search more broadly for WasmLanguageError
echo "=== Searching for WasmLanguageError across the codebase ==="
rg 'WasmLanguageError' rust-executor/src/wasm_core/mod.rs | head -10
echo ""
echo "=== Looking at the return type context of register_wasm_language ==="
rg -B 1 -A 15 'pub fn register_wasm_language' rust-executor/src/wasm_core/mod.rs | head -25Repository: coasys/ad4m
Length of output: 1490
🏁 Script executed:
#!/bin/bash
set -euo pypipefail
# Check if there's an error conversion or if the types are compatible
echo "=== Checking for error type definitions and implementations ==="
rg -n 'type.*Error|enum.*Error' rust-executor/src/wasm_core/mod.rs | head -15
echo ""
echo "=== Looking at how other functions handle wasm_core calls ==="
rg -B 2 -A 2 'wasm_core::' rust-executor/src/languages/mod.rsRepository: coasys/ad4m
Length of output: 110
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check for error type conversions
echo "=== Looking at error module and WasmLanguageError definition ==="
rg -n 'mod error|pub mod error' rust-executor/src/wasm_core/mod.rs
echo ""
echo "=== Searching for From/Into implementations for WasmLanguageError ==="
rg 'impl.*From.*for.*WasmLanguageError|impl.*Into.*WasmLanguageError|From<WasmLanguageError>' rust-executor/src/wasm_core/
echo ""
echo "=== Checking what AnyError is ==="
rg -n 'type AnyError|use.*AnyError' rust-executor/src/languages/mod.rs | head -5Repository: coasys/ad4m
Length of output: 655
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the error module in wasm_core
echo "=== Looking at wasm_core error module ==="
fd -t f error.rs rust-executor/src/wasm_core/ && cat rust-executor/src/wasm_core/error.rs | head -50Repository: coasys/ad4m
Length of output: 1890
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Let's look at the full error.rs file to find the blanket impl
echo "=== Checking full error.rs for From/Into implementations ==="
wc -l rust-executor/src/wasm_core/error.rs
echo ""
echo "=== Looking for blanket impl and Into implementations ==="
rg -n 'impl.*From.*AnyError|impl.*std::error::Error|fn from' rust-executor/src/wasm_core/error.rs | tail -20Repository: coasys/ad4m
Length of output: 422
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Get the full error.rs to see all implementations
echo "=== Viewing the end of error.rs to see blanket impl ==="
tail -100 rust-executor/src/wasm_core/error.rsRepository: coasys/ad4m
Length of output: 3677
install_wasm_from_base64 calls an undefined method.
Line 905 invokes Self::install_wasm_language(...), but this method does not exist in the LanguageController impl, which breaks wasm-languages builds.
🔧 Proposed fix
- Self::install_wasm_language(std::path::Path::new(&bundle_path), address)?;
+ crate::wasm_core::register_wasm_language(std::path::Path::new(&bundle_path), address)?;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async fn install_wasm_from_base64(base64_data: &str, address: &str) -> Result<(), AnyError> { | |
| use base64::Engine; | |
| let wasm_bytes = base64::engine::general_purpose::STANDARD | |
| .decode(base64_data) | |
| .map_err(|e| deno_core::anyhow::anyhow!("Base64 decode error: {}", e))?; | |
| // Verify WASM magic | |
| if wasm_bytes.len() < 4 || &wasm_bytes[0..4] != b"\0asm" { | |
| return Err(deno_core::anyhow::anyhow!("Decoded data is not valid WASM")); | |
| } | |
| // Save to languages directory | |
| let languages_path = Self::languages_path(); | |
| let lang_dir = format!("{}/{}", languages_path, address); | |
| std::fs::create_dir_all(&lang_dir)?; | |
| let bundle_path = format!("{}/bundle.wasm", lang_dir); | |
| std::fs::write(&bundle_path, &wasm_bytes)?; | |
| log::info!("Saved WASM bundle ({} bytes) to {}", wasm_bytes.len(), bundle_path); | |
| // Register in WASM runtime | |
| Self::install_wasm_language(std::path::Path::new(&bundle_path), address)?; | |
| Ok(()) | |
| } | |
| async fn install_wasm_from_base64(base64_data: &str, address: &str) -> Result<(), AnyError> { | |
| use base64::Engine; | |
| let wasm_bytes = base64::engine::general_purpose::STANDARD | |
| .decode(base64_data) | |
| .map_err(|e| deno_core::anyhow::anyhow!("Base64 decode error: {}", e))?; | |
| // Verify WASM magic | |
| if wasm_bytes.len() < 4 || &wasm_bytes[0..4] != b"\0asm" { | |
| return Err(deno_core::anyhow::anyhow!("Decoded data is not valid WASM")); | |
| } | |
| // Save to languages directory | |
| let languages_path = Self::languages_path(); | |
| let lang_dir = format!("{}/{}", languages_path, address); | |
| std::fs::create_dir_all(&lang_dir)?; | |
| let bundle_path = format!("{}/bundle.wasm", lang_dir); | |
| std::fs::write(&bundle_path, &wasm_bytes)?; | |
| log::info!("Saved WASM bundle ({} bytes) to {}", wasm_bytes.len(), bundle_path); | |
| // Register in WASM runtime | |
| crate::wasm_core::register_wasm_language(std::path::Path::new(&bundle_path), address)?; | |
| Ok(()) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@rust-executor/src/languages/mod.rs` around lines 884 - 907,
install_wasm_from_base64 calls a non-existent Self::install_wasm_language; add
an implementation of install_wasm_language to the LanguageController impl (or
rename the call to the correct existing method) so the saved bundle is
registered with the WASM runtime. Specifically, add a method with signature like
fn install_wasm_language(path: &std::path::Path, address: &str) -> Result<(),
AnyError> on LanguageController that loads the bundle at bundle_path,
validates/initializes it with the runtime registration code used elsewhere in
the module, returns errors as AnyError, and is invoked from
install_wasm_from_base64; alternatively, if a registration helper already exists
under a different name, replace the Self::install_wasm_language call with that
helper (keeping the same path/address arguments).
| lazy_static! { | ||
| static ref APP_DATA_PATH: std::sync::RwLock<Option<String>> = std::sync::RwLock::new(None); | ||
| } | ||
|
|
||
| pub fn set_app_data_path(path: String) { | ||
| let mut data_path = APP_DATA_PATH.write().unwrap(); | ||
| *data_path = Some(path); | ||
| } | ||
|
|
||
| fn get_app_data_path() -> Option<String> { | ||
| APP_DATA_PATH.read().unwrap().clone() | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="rust-executor/src/perspectives/mod.rs"
echo "=== Searching for APP_DATA_PATH ==="
rg -n "APP_DATA_PATH" "$file" || echo "No matches found"
echo ""
echo "=== Searching for set_app_data_path ==="
rg -n "set_app_data_path" "$file" || echo "No matches found"
echo ""
echo "=== Searching for get_app_data_path ==="
rg -n "get_app_data_path" "$file" || echo "No matches found"
echo ""
echo "=== File line count ==="
wc -l "$file"Repository: coasys/ad4m
Length of output: 964
Remove duplicate symbol definitions that cause Rust compilation to fail.
Lines 819–830 redefine APP_DATA_PATH, set_app_data_path, and get_app_data_path that are already defined earlier in the module (lines 27, 40, and 50 respectively). Rust does not allow duplicate definitions in the same module scope. Remove the duplicate block at lines 819–830.
Duplicate definitions to remove
-lazy_static! {
- static ref APP_DATA_PATH: std::sync::RwLock<Option<String>> = std::sync::RwLock::new(None);
-}
-
-pub fn set_app_data_path(path: String) {
- let mut data_path = APP_DATA_PATH.write().unwrap();
- *data_path = Some(path);
-}
-
-fn get_app_data_path() -> Option<String> {
- APP_DATA_PATH.read().unwrap().clone()
-}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| lazy_static! { | |
| static ref APP_DATA_PATH: std::sync::RwLock<Option<String>> = std::sync::RwLock::new(None); | |
| } | |
| pub fn set_app_data_path(path: String) { | |
| let mut data_path = APP_DATA_PATH.write().unwrap(); | |
| *data_path = Some(path); | |
| } | |
| fn get_app_data_path() -> Option<String> { | |
| APP_DATA_PATH.read().unwrap().clone() | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@rust-executor/src/perspectives/mod.rs` around lines 819 - 830, The block
redefines APP_DATA_PATH, set_app_data_path, and get_app_data_path which
duplicates earlier definitions and causes compilation failure; remove the
duplicated lazy_static! and the two functions (the block containing
APP_DATA_PATH, set_app_data_path, get_app_data_path) so only the original
definitions remain, ensuring no other references rely on the duplicate names.
| sdna_change_mutex: Arc<Mutex<()>>, | ||
| prolog_update_mutex: Arc<RwLock<()>>, | ||
| link_language: Arc<RwLock<Option<Language>>>, | ||
| link_language: Arc<RwLock<Option<Arc<Mutex<Box<dyn LanguageBackend>>>>>>, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Check for wrapper impl of LanguageBackend on Arc<Mutex<Box<dyn LanguageBackend>>> =="
rg -nP --type rust 'impl\s+.*LanguageBackend\s+for\s+Arc<\s*Mutex<\s*Box<\s*dyn\s+LanguageBackend\s*>\s*>\s*>'
echo
echo "== Direct link_language method calls in perspective_instance.rs =="
rg -nP -C2 --type rust '\blink_language\.(sync|commit|current_revision|render|others|get_online_agents|set_online_status|send_signal|send_broadcast|has_telepresence_adapter)\s*\(' rust-executor/src/perspectives/perspective_instance.rs
echo
echo "== Lock-based backend method calls in perspective_instance.rs =="
rg -nP -C2 --type rust '\bll\.(sync|commit|current_revision|render|others|get_online_agents|set_online_status|send_signal|send_broadcast|has_telepresence_adapter)\s*\(' rust-executor/src/perspectives/perspective_instance.rsRepository: coasys/ad4m
Length of output: 142
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# First, understand the file structure and find link_language usage
echo "== File info =="
wc -l rust-executor/src/perspectives/perspective_instance.rs
echo
echo "== Line 185 context (the type definition) =="
sed -n '180,190p' rust-executor/src/perspectives/perspective_instance.rs
echo
echo "== All link_language usages in the file =="
rg -n 'link_language' rust-executor/src/perspectives/perspective_instance.rs | head -50
echo
echo "== Check if link_language is locked/unwrapped before method calls =="
rg -nB2 -A2 'link_language\s*=' rust-executor/src/perspectives/perspective_instance.rs | head -100Repository: coasys/ad4m
Length of output: 4188
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Lines 590-660 context (around direct link_language calls) =="
sed -n '590,660p' rust-executor/src/perspectives/perspective_instance.rs
echo
echo "== Lines 715-750 context (another usage block) =="
sed -n '715,750p' rust-executor/src/perspectives/perspective_instance.rsRepository: coasys/ad4m
Length of output: 4667
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for all direct method calls on link_language without lock pattern =="
rg -nB3 'link_language\.(current_revision|render|commit|others|sync|get_online_agents|set_online_status)' rust-executor/src/perspectives/perspective_instance.rs
echo
echo "== Verify no direct calls outside proper lock context =="
rg -nB5 'if let Some.*link_language.*=' rust-executor/src/perspectives/perspective_instance.rs | grep -A5 'current_revision\|render\|commit'Repository: coasys/ad4m
Length of output: 654
🏁 Script executed:
#!/bin/bash
set -euo pipeftp
echo "== Find all suspicious patterns: if let Some(...link_language...) followed by direct method calls =="
rg -n 'if let Some.*link_language' rust-executor/src/perspectives/perspective_instance.rs | while read line; do
linenum=$(echo "$line" | cut -d: -f1)
start=$((linenum))
end=$((linenum + 20))
echo "=== Context from line $start to $end ==="
sed -n "${start},${end}p" rust-executor/src/perspectives/perspective_instance.rs
echo
done
echo "== Count of lock-based vs direct-call patterns =="
echo "Direct calls (NO lock):"
rg -c 'if let Some.*link_language.*=.*\n.*link_language\.(current_revision|render|commit|others|sync)' rust-executor/src/perspectives/perspective_instance.rs || echo "0"
echo "Lock-based calls (WITH lock):"
rg -c 'let mut ll = link_language\.lock\(\)\.await' rust-executor/src/perspectives/perspective_instance.rsRepository: coasys/ad4m
Length of output: 107
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Find all suspicious patterns: if let Some(...link_language...) followed by direct method calls =="
rg -n 'if let Some.*link_language' rust-executor/src/perspectives/perspective_instance.rs | while read line; do
linenum=$(echo "$line" | cut -d: -f1)
start=$((linenum))
end=$((linenum + 20))
echo "=== Context from line $start to $end ==="
sed -n "${start},${end}p" rust-executor/src/perspectives/perspective_instance.rs
echo
doneRepository: coasys/ad4m
Length of output: 9978
Fix link_language mutex acquisition at 4 call sites
The type Arc<RwLock<Option<Arc<Mutex<Box<dyn LanguageBackend>>>>>> requires acquiring the inner mutex before calling backend methods. Four locations call methods directly without locking:
- Lines 598–648 (
ensure_public_links_are_shared): Callscurrent_revision(),render(), andcommit()directly onlink_language - Line 3058 (
others): Callsothers()directly onlink_language - Line 3211 (
send_signal): Callssend_signal()directly onlink_language - Line 3319 (
update_local_agents): Callsset_local_agents()directly onlink_language
Use the correct pattern from lines 434, 529, 723, etc.: let mut ll = link_language.lock().await; before calling any backend methods.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@rust-executor/src/perspectives/perspective_instance.rs` at line 185, The
calls that invoke backend methods on link_language must first acquire the inner
mutex instead of calling methods directly; at each site
(ensure_public_links_are_shared, others, send_signal, update_local_agents)
obtain the RwLock read (or write) guard on link_language, check Option with if
let Some(ll_arc) = guard.as_ref(), then await the inner Mutex lock (e.g., let
mut ll = ll_arc.lock().await) and call current_revision/render/commit, others,
send_signal, or set_local_agents on ll; follow the existing pattern used at
lines ~434/529/723 to avoid direct method calls on the Option without locking.
| fn read_guest_bytes(view: &MemoryView, ptr: u32, len: u32) -> Result<Vec<u8>, WasmLanguageError> { | ||
| let mut buf = vec![0u8; len as usize]; | ||
| view.read(ptr as u64, &mut buf) | ||
| .map_err(|e| WasmLanguageError::MemoryAccessError(format!("read failed: {}", e)))?; | ||
| Ok(buf) |
There was a problem hiding this comment.
Validate guest-provided lengths before host allocation.
At Line 67, len comes from guest memory metadata. A malicious or corrupted module can force very large host allocations (Vec<u8>), causing OOM/DoS. Add strict upper bounds and checked arithmetic before allocation/read.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@rust-executor/src/wasm_core/mod.rs` around lines 66 - 70, In
read_guest_bytes, validate the guest-provided len and pointer before allocating:
introduce a safe MAX_GUEST_READ constant (e.g. a few MB) and reject any len == 0
or len > MAX_GUEST_READ by returning WasmLanguageError::MemoryAccessError; use
checked arithmetic (checked_add/checked_mul/checked_into) to compute ptr+len and
only convert len to usize after those checks; also verify the requested range
fits inside the MemoryView (use view.size() or an equivalent API) before
creating the Vec<u8> and calling view.read so no unbounded or overflowing
allocation can occur.
| let multihash = Code::Sha2_256.digest(data_str.as_bytes()); | ||
| let cid = Cid::new_v1(0, multihash); | ||
| let encoded_cid = multibase::encode(Base::Base58Btc, cid.to_bytes()); | ||
| let hash_str = format!("Qm{}", encoded_cid); | ||
| let json = match serde_json::to_vec(&hash_str) { |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
multibase Rust encode function Base::Base58Btc prefix multibase
💡 Result:
In Rust’s multibase crate, encode (and Base::encode) returns a multibase string including the 1-character prefix.
use multibase::{Base, encode};
let s1 = encode(Base::Base58Btc, b"hello");
let s2 = Base::Base58Btc.encode(b"hello");
assert_eq!(s1, "zCn8eVZg");
assert_eq!(s1, s2);For base58btc, the multibase prefix is 'z', so encoded strings will start with z.... [1][2]
Sources: [1] multibase::encode docs/examples, [2] multibase prefix table (base58btc → z).
🌐 Web query:
CID v1 IPFS Qm prefix Base58Btc multibase encoding
💡 Result:
Qm…is not CIDv1: a 46-character IPFS CID starting withQmis a CIDv0, encoded in base58btc, and it’s essentially just a multihash (with the IPFS defaults implicitly assumed). [1]- Why CIDv0 often starts with
Qm: in CIDv0, the multihash prefix bytes for sha2-256 + 32-byte digest length are fixed, and when that byte sequence is base58btc-encoded it commonly renders with aQmprefix. [1] - CIDv1 string form always has an explicit multibase prefix: CIDv1 includes leading identifiers (e.g., multibase + version + multicodec + multihash). [1]
- If a CIDv1 is base58btc-encoded, it starts with
z…, notQm…:zis the multibase code for base58btc; example CIDv1 strings look likez…. [2][3] - So:
Qm…⇒ CIDv0 base58btc (no multibase prefix);z…⇒ CIDv1 base58btc (multibase prefix included);b…(oftenbafy…) ⇒ CIDv1 base32 (common default in many implementations). [2][1]
Sources:
[1] IPFS Docs – Content Identifiers (CIDs) (CIDv0 Qm…, CIDv1 has multibase prefix) (docs.ipfs.tech)
[2] ProtoSchool – Anatomy of a CID (multibase prefix; Qm=CIDv0, b=base32 CIDv1) (proto.school)
[3] multiformats CID docs – example CIDv1 base58btc uses z… prefix (multiformats.readthedocs.io)
Citations:
- 1: https://docs.ipfs.tech/concepts/content-addressing/?utm_source=openai
- 2: https://proto.school/anatomy-of-a-cid/05/?utm_source=openai
- 3: https://multiformats.readthedocs.io/en/latest/api/multiformats.cid.html?utm_source=openai
🏁 Script executed:
find . -name "*.rs" -type f | head -20Repository: coasys/ad4m
Length of output: 1703
🏁 Script executed:
cat -n rust-executor/src/wasm_core/mod.rs | sed -n '320,340p'Repository: coasys/ad4m
Length of output: 872
🏁 Script executed:
rg "host_hash" rust-executor/src/wasm_core/mod.rs -B 5 -A 10Repository: coasys/ad4m
Length of output: 3406
🏁 Script executed:
rg "host_hash" --type rs -l | head -10Repository: coasys/ad4m
Length of output: 81
🏁 Script executed:
rg "host_hash" --type rs -A 3 | head -40Repository: coasys/ad4m
Length of output: 81
🏁 Script executed:
rg "host_hash\|IPFS-compatible" rust-executor/src/wasm_core/mod.rs -B 2 -A 15Repository: coasys/ad4m
Length of output: 37
🏁 Script executed:
cd rust-executor && grep -r "host_hash\|Qm" --include="*.rs" | head -20Repository: coasys/ad4m
Length of output: 1057
🏁 Script executed:
cd rust-executor && cat -n src/js_core/utils_extension.rs | sed -n '1,50p'Repository: coasys/ad4m
Length of output: 1668
🏁 Script executed:
cd rust-executor && grep -B 10 -A 3 'format!("Qm' src/js_core/utils_extension.rs | head -30Repository: coasys/ad4m
Length of output: 506
🏁 Script executed:
cd rust-executor && grep -B 5 -A 5 'format!("Qm' src/languages/mod.rsRepository: coasys/ad4m
Length of output: 447
🏁 Script executed:
cd rust-executor && rg "Cid::new_v1" --type rs -B 2 -A 2Repository: coasys/ad4m
Length of output: 81
🏁 Script executed:
cd rust-executor && grep -r "Cid::new_v1" --include="*.rs" -B 2 -A 2Repository: coasys/ad4m
Length of output: 967
🏁 Script executed:
cd rust-executor && grep -r "Cid::new_v0\|CidVersion" --include="*.rs" | head -10Repository: coasys/ad4m
Length of output: 37
CIDv1 encoding produces malformed hash strings due to double-prefixing.
multibase::encode(Base::Base58Btc, ...) returns a string with the multibase prefix included (e.g., "z..."). Prepending "Qm" to this result creates a malformed hash like "Qmz..." instead of a valid CIDv1 (which should be "z...") or CIDv0 (which should be raw base58btc without multibase encoding).
This pattern occurs in three functions:
host_hash()inwasm_core/mod.rs(line 332)hash()injs_core/utils_extension.rs(line 20)hash_language()inlanguages/mod.rs
All three need correcting to match the intended output format (either proper CIDv0 or CIDv1).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@rust-executor/src/wasm_core/mod.rs` around lines 329 - 333, The code is
prepending "Qm" to a multibase-encoded CID which double-prefixes and produces
malformed hashes; in host_hash(), hash(), and hash_language() update the logic
to either emit a valid CIDv1 by using the multibase::encode(...) result as-is
(do not prepend "Qm"), or if you need CIDv0 construct a CIDv0 (e.g.,
Cid::new_v0(...) or use the CID API that returns v0) and use its string/base58
representation (cid.to_string() or the v0-specific API) instead of
multibase::encode + "Qm"—remove the manual "Qm" prefix and choose the correct
CID version API for each function (host_hash, hash in js_core::utils_extension,
and hash_language in languages::mod) so the produced strings are valid CIDs.
| let (ptr, len) = self.write_input(&input)?; | ||
| let func: TypedFunction<(u32, u32), u64> = self | ||
| .instance | ||
| .exports | ||
| .get_typed_function(&self.store, "ad4m_expression_get") | ||
| .map_err(|e| WasmLanguageError::MissingExport(format!("ad4m_expression_get: {}", e)))?; | ||
| let result = func | ||
| .call(&mut self.store, ptr, len) | ||
| .map_err(|e| WasmLanguageError::RuntimeError(format!("{}", e)))?; | ||
| if result == 0 { | ||
| return Ok(None); | ||
| } | ||
| let bytes = self.read_result(result)?; | ||
| if bytes.is_empty() { | ||
| return Ok(None); | ||
| } | ||
| // Try to deserialise; if it's a null JSON value, return None | ||
| let value: serde_json::Value = from_json_bytes(&bytes)?; | ||
| if value.is_null() { | ||
| return Ok(None); | ||
| } | ||
| let expr: AbiExpression = serde_json::from_value(value)?; | ||
| Ok(Some(expr)) | ||
| } |
There was a problem hiding this comment.
Guest memory is leaked because input/output buffers are never deallocated.
Across these call paths, write_input() allocates guest memory, and read_result() reads guest-returned buffers, but neither input nor output pointers are released with ad4m_dealloc. Repeated calls will grow linear memory indefinitely.
♻️ Suggested fix pattern (apply to each call site)
let (ptr, len) = self.write_input(&input)?;
let result = func.call(&mut self.store, ptr, len)
.map_err(|e| WasmLanguageError::RuntimeError(format!("{}", e)))?;
+// release input buffer
+self.dealloc(ptr, len)?;
let bytes = self.read_result(result)?;
+// release output buffer
+let (out_ptr, out_len) = decode_fat_ptr(result);
+if out_ptr != 0 && out_len != 0 {
+ self.dealloc(out_ptr, out_len)?;
+}Also applies to: 850-863, 873-889, 936-949, 983-1001
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@rust-executor/src/wasm_core/mod.rs` around lines 815 - 838, The guest memory
allocated by write_input(ptr,len) and the buffer returned by ad4m_expression_get
(consumed via read_result(result)) must be freed using the guest deallocator
export (ad4m_dealloc). After calling the wasm function (ad4m_expression_get) and
before returning, call the exported ad4m_dealloc for the input pointer/length
produced by write_input; also ensure you parse the result pointer/length from
result and call ad4m_dealloc on that guest buffer after read_result returns (or
immediately after reading its bytes). Apply this pattern to the
ad4m_expression_get call site shown and the other listed call sites (lines
around 850-863, 873-889, 936-949, 983-1001) by locating write_input, read_result
and the wasm export invocations and inserting calls to the exported ad4m_dealloc
function to release both input and output guest memory.
Rebased on dev (post language controller refactor). See #692 for original discussion.
Changes
rust-executor/src/wasm_core/)wasm-language-sdk/)LanguageBackendtrait for dual JS/WASM backendsSupersedes #692.
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Documentation
Chores