Skip to content

feat: WASM-based language execution runtime#707

Draft
HexaField wants to merge 27 commits intocoasys:devfrom
HexaField:feat/wasm-language-runtime
Draft

feat: WASM-based language execution runtime#707
HexaField wants to merge 27 commits intocoasys:devfrom
HexaField:feat/wasm-language-runtime

Conversation

@HexaField
Copy link
Contributor

@HexaField HexaField commented Mar 4, 2026

Rebased on dev (post language controller refactor). See #692 for original discussion.

Changes

  • WASM runtime (rust-executor/src/wasm_core/)
  • WASM language SDK (wasm-language-sdk/)
  • Example WASM languages
  • LanguageBackend trait for dual JS/WASM backends
  • Memory leak fixes (teardown, signal cleanup, SurrealDB)
  • Profiling infrastructure and CI workflows

Supersedes #692.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added WASM language runtime support for building and running languages as WebAssembly modules.
    • Added ability to install and publish WASM languages to the network.
    • Included example WASM language implementations for reference.
  • Bug Fixes

    • Fixed memory leaks and improved resource cleanup across perspectives, languages, and background processes.
  • Documentation

    • Added comprehensive memory profiling guides and leak investigation reports.
  • Chores

    • Added CI workflow for validating WASM builds.
    • Introduced feature flags for optional backends and language support.

HexaField and others added 27 commits March 4, 2026 11:00
- 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.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
WASM Language SDK
wasm-language-sdk/Cargo.toml, wasm-language-sdk/src/lib.rs, wasm-language-sdk/src/host.rs, wasm-language-sdk/src/memory.rs, wasm-language-sdk/src/types.rs
New Rust library providing SDK for building AD4M WASM languages; defines core data types (Expression, Link, PerspectiveDiff), trait interfaces for language capabilities (ExpressionLanguage, LinksAdapter, LanguageInteractions), host function bindings (signing, hashing, Holochain calls), memory management (fat pointers, allocation), and export-generation macros.
WASM Runtime Core
rust-executor/src/wasm_core/README.md, rust-executor/src/wasm_core/abi.rs, rust-executor/src/wasm_core/error.rs, rust-executor/src/wasm_core/mod.rs, rust-executor/src/wasm_core/tests.rs
Comprehensive WASM runtime loader and executor; includes ABI versioning, fat-pointer encoding, error types, Wasmer-based module loading, host environment with 12+ host functions, registry system, capability detection, and extensive tests for note-store, link-store, and p-diff-sync examples.
WASM Language Examples
examples/wasm-languages/note-store/*, examples/wasm-languages/link-store/*, examples/wasm-languages/p-diff-sync-wasm/*
Three example WASM language implementations: in-memory note storage with JSON serialization, in-memory link store with revision tracking, and Holochain-backed p-diff-sync with DNA bundle embedding and Zome calls.
Executor Integration
rust-executor/src/languages/language.rs, rust-executor/src/languages/mod.rs, rust-executor/src/graphql/mutation_resolvers.rs, rust-executor/src/js_core/languages_extension.rs, rust-executor/src/js_core/languages_extension.js, rust-executor/src/lib.rs
New LanguageBackend trait abstraction; WasmLanguage implementation of LanguageBackend; GraphQL mutations for language_install_wasm and language_publish_wasm; helper methods for WASM loading, base64 detection, and metadata handling; feature-gated module exports.
Feature Flags & Configuration
Cargo.toml, cli/Cargo.toml, rust-executor/Cargo.toml, rust-executor/build.rs
New features: wasm-languages (enables WASM runtime), surrealdb-links (enables SurrealDB storage backend), sqlite-links (enables SQLite backend); sets surrealdb-links as default feature; adds async-trait dependency; adds rerun-if-changed for CUSTOM_DENO_SNAPSHOT.bin in build script.
Perspective & Service Lifecycle
rust-executor/src/perspectives/perspective_instance.rs, rust-executor/src/perspectives/mod.rs, rust-executor/src/holochain_service/mod.rs, rust-executor/src/surreal_service/mod.rs, rust-executor/src/prolog_service/mod.rs
Replaces Language with LanguageBackend trait in PerspectiveInstance; adds comprehensive teardown with Prolog cleanup, SurrealDB shutdown, language unloading; adds APP_DATA_PATH configuration; implements signal stream cleanup on app removal; exposes remove_perspective_pool API; adds SurrealDBService shutdown method.
Memory Profiling & Investigation
docs/profiling/README.md, docs/profiling/leak-investigation-2026-02-21.md, docs/profiling/leak-investigation.mjs, docs/profiling/profiler-v9.mjs, docs/profiling/profiling-results-2026-02-21.md, docs/profiling/publish-langs.mjs, docs/profiling/refactoring-plan.md
New memory profiling suite: investigation scripts measuring RSS/smaps across 5 test scenarios (perspectives, neighbourhoods, language cloning, links, queries), detailed results with per-stage breakdown, comprehensive refactoring plan documenting root causes (Holochain memory retention, perspective/language leaks) and prioritized fixes.
CI/CD
.github/workflows/exploration-ci.yml, rust-client/schema.gql
New GitHub Actions workflow (Exploration CI) triggering on feat/wasm-language-runtime and feat/sqlite-link-storage branches; runs wasm-sdk build, cargo-check with feature flags, and rust-tests with conditional wasm-languages feature; includes WASM export verification via wabt; updates rust-client schema path reference.
Integration Testing
tests/js/wasm-integration-test.mjs
End-to-end WASM integration test: validates local bundle installation, expression operations, language source queries, perspective workflows with WASM links, language publishing, base64/magic-byte validation, and memory stability checks via RSS measurement.

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
Loading
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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • lucksus
  • jhweir

Poem

🐰 Hops through WASM gateways with glee,
Languages compiled in binary!
SDK macros dance, memories leak no more,
Host functions call from the other shore, 🌊
A rabbit's dream: polyglot languages soar!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: WASM-based language execution runtime' clearly and concisely summarizes the main change: introducing a new WASM-based runtime for executing AD4M languages, which is the primary focus of this large changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔴 Critical

Import 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 a ReferenceError when 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 | 🔴 Critical

Register install_wasm_language in the extension ops list.

install_wasm_language is defined at lines 83–98 with #[op2] decorator but missing from the ops list in deno_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 | 🔴 Critical

Escape remote_agent_did before embedding into JS source

Line 190 injects remote_agent_did directly 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 | 🟠 Major

Use try/finally to 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 | 🟠 Major

Guarantee teardown with try/finally for 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 | 🟠 Major

Host 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 | 🟠 Major

Guarantee subprocess cleanup on all exit paths.

After startup (Line 101 onward), any uncaught error before Line 220 leaves child/bootstrap alive because Line 226 exits immediately. Wrap runtime logic in try/finally and terminate both processes in finally.

💡 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 | 🟠 Major

Re-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 | 🟠 Major

Avoid holding registry lock while running teardown.

unregister_wasm_language keeps 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 | 🟠 Major

Guarantee 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 | 🟠 Major

Validate 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 | 🟠 Major

Make 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 overwrite app_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 | 🟠 Major

Don’t drop link_language reference after failed language_remove

At 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 | 🟠 Major

Don’t mark the DNA as removed when uninstall fails

At Line 225, uninstall errors are ignored, but Line 227 still flips installed to false. 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 | 🟠 Major

Avoid 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 | 🟠 Major

Don’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 | 🟠 Major

Validate numeric byte ranges before converting to u8

Lines 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 in holochain_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 | 🟠 Major

Replace lock().unwrap() with error propagation

Lines 264, 272, 310, 318, and 347 use lock().unwrap() on a Mutex. 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 | 🟡 Minor

Update 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 in new()) when data_path is provided. In-memory storage is only a fallback when data_path is None (tests only). Update the comment to accurately reflect the RocksDB-based persistence in production.

The current &self signature is correct given the architecture: SurrealDBService is Arc-wrapped and owned by PerspectiveInstance, so the Arc is dropped when the instance is removed from the PERSPECTIVES map in remove_perspective(), not by shutdown() 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 | 🟡 Minor

Avoid logging the raw admin credential in command output.

Line 108 logs --admin-credential into 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 | 🟡 Minor

Fail 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 | 🟡 Minor

Document 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 | 🟡 Minor

Add 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 | 🟡 Minor

Specify 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.

💡 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
+}
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".

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, and hc_get_agent_key to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6dfad94 and bb0ec13.

⛔ Files ignored due to path filters (6)
  • examples/wasm-languages/link-store/Cargo.lock is excluded by !**/*.lock
  • examples/wasm-languages/note-store/Cargo.lock is excluded by !**/*.lock
  • examples/wasm-languages/p-diff-sync-wasm/Cargo.lock is excluded by !**/*.lock
  • rust-executor/tests/fixtures/wasm/link_store_wasm.wasm is excluded by !**/*.wasm
  • rust-executor/tests/fixtures/wasm/note_store_wasm.wasm is excluded by !**/*.wasm
  • wasm-language-sdk/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (41)
  • .github/workflows/exploration-ci.yml
  • Cargo.toml
  • cli/Cargo.toml
  • docs/profiling/README.md
  • docs/profiling/leak-investigation-2026-02-21.md
  • docs/profiling/leak-investigation.mjs
  • docs/profiling/profiler-v9.mjs
  • docs/profiling/profiling-results-2026-02-21.md
  • docs/profiling/publish-langs.mjs
  • docs/profiling/refactoring-plan.md
  • examples/wasm-languages/link-store/Cargo.toml
  • examples/wasm-languages/link-store/src/lib.rs
  • examples/wasm-languages/note-store/Cargo.toml
  • examples/wasm-languages/note-store/src/lib.rs
  • examples/wasm-languages/p-diff-sync-wasm/Cargo.toml
  • examples/wasm-languages/p-diff-sync-wasm/src/lib.rs
  • rust-client/schema.gql
  • rust-executor/Cargo.toml
  • rust-executor/build.rs
  • rust-executor/src/graphql/mutation_resolvers.rs
  • rust-executor/src/holochain_service/mod.rs
  • rust-executor/src/js_core/languages_extension.js
  • rust-executor/src/js_core/languages_extension.rs
  • rust-executor/src/languages/language.rs
  • rust-executor/src/languages/mod.rs
  • rust-executor/src/lib.rs
  • rust-executor/src/perspectives/mod.rs
  • rust-executor/src/perspectives/perspective_instance.rs
  • rust-executor/src/prolog_service/mod.rs
  • rust-executor/src/surreal_service/mod.rs
  • rust-executor/src/wasm_core/README.md
  • rust-executor/src/wasm_core/abi.rs
  • rust-executor/src/wasm_core/error.rs
  • rust-executor/src/wasm_core/mod.rs
  • rust-executor/src/wasm_core/tests.rs
  • tests/js/wasm-integration-test.mjs
  • wasm-language-sdk/Cargo.toml
  • wasm-language-sdk/src/host.rs
  • wasm-language-sdk/src/lib.rs
  • wasm-language-sdk/src/memory.rs
  • wasm-language-sdk/src/types.rs

Comment on lines +31 to +38
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"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -60

Repository: 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.")
PY

Repository: coasys/ad4m

Length of output: 303


🏁 Script executed:

#!/bin/bash
# Check for surrealdb in the dependencies
grep -n "surrealdb" rust-executor/Cargo.toml

Repository: 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 -80

Repository: 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).

Comment on lines +1603 to +1610
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -30

Repository: 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 -n

Repository: 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.

Suggested change
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.

Comment on lines +843 to +874
/// 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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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.rs

Repository: 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.

Comment on lines +884 to +907
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(())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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.rs

Repository: 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 -n

Repository: 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.rs

Repository: 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.rs

Repository: 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 -20

Repository: 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 -20

Repository: 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.rs

Repository: 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 -25

Repository: 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.rs

Repository: 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 -5

Repository: 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 -50

Repository: 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 -20

Repository: 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.rs

Repository: 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.

Suggested change
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).

Comment on lines +819 to +830
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()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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.

Suggested change
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>>>>>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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.rs

Repository: 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 -100

Repository: 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.rs

Repository: 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.rs

Repository: 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
done

Repository: 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): Calls current_revision(), render(), and commit() directly on link_language
  • Line 3058 (others): Calls others() directly on link_language
  • Line 3211 (send_signal): Calls send_signal() directly on link_language
  • Line 3319 (update_local_agents): Calls set_local_agents() directly on link_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.

Comment on lines +66 to +70
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +329 to +333
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 with Qm is 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 a Qm prefix. [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…, not Qm…: z is the multibase code for base58btc; example CIDv1 strings look like z…. [2][3]
  • So: Qm…CIDv0 base58btc (no multibase prefix); z…CIDv1 base58btc (multibase prefix included); b… (often bafy…) ⇒ 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:


🏁 Script executed:

find . -name "*.rs" -type f | head -20

Repository: 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 10

Repository: coasys/ad4m

Length of output: 3406


🏁 Script executed:

rg "host_hash" --type rs -l | head -10

Repository: coasys/ad4m

Length of output: 81


🏁 Script executed:

rg "host_hash" --type rs -A 3 | head -40

Repository: coasys/ad4m

Length of output: 81


🏁 Script executed:

rg "host_hash\|IPFS-compatible" rust-executor/src/wasm_core/mod.rs -B 2 -A 15

Repository: coasys/ad4m

Length of output: 37


🏁 Script executed:

cd rust-executor && grep -r "host_hash\|Qm" --include="*.rs" | head -20

Repository: 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 -30

Repository: coasys/ad4m

Length of output: 506


🏁 Script executed:

cd rust-executor && grep -B 5 -A 5 'format!("Qm' src/languages/mod.rs

Repository: coasys/ad4m

Length of output: 447


🏁 Script executed:

cd rust-executor && rg "Cid::new_v1" --type rs -B 2 -A 2

Repository: coasys/ad4m

Length of output: 81


🏁 Script executed:

cd rust-executor && grep -r "Cid::new_v1" --include="*.rs" -B 2 -A 2

Repository: coasys/ad4m

Length of output: 967


🏁 Script executed:

cd rust-executor && grep -r "Cid::new_v0\|CidVersion" --include="*.rs" | head -10

Repository: 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() in wasm_core/mod.rs (line 332)
  • hash() in js_core/utils_extension.rs (line 20)
  • hash_language() in languages/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.

Comment on lines +815 to +838
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))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

@HexaField HexaField marked this pull request as draft March 4, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant