dwara_core/wasm/
host.rs

1//! proxy-wasm host implementation (DW-055).
2//!
3//! This module implements the host side of the proxy-wasm ABI: the
4//! import functions that a WebAssembly plugin calls to interact with
5//! the proxy. The plugin exports phase callbacks (e.g.
6//! `proxy_on_request_headers`) that the host calls at the appropriate
7//! point in the request pipeline; the plugin calls back into the host
8//! via the imports defined here to read/write request data, log, send
9//! responses, etc.
10//!
11//! ## Architecture
12//!
13//! - [`WasmEngine`] — process-wide wasmtime engine + linker, compiled
14//!   once at startup. Holds the compiled modules keyed by config name.
15//!   Cheap to clone (Arc internals).
16//! - [`PluginModule`] — a compiled wasmtime module for one plugin
17//!   config entry. Created once at config publish time.
18//! - [`PluginInstance`] — a per-request plugin instance (store +
19//!   instance + context). Created for each request that passes through
20//!   a route with plugins.
21//! - [`PluginContext`] — the per-instance state the host imports read
22//!   from and write to: request/response headers, body, the action
23//!   returned by the plugin, etc. Stored in the wasmtime store's
24//!   user data.
25//!
26//! ## Fuel and epoch preemption
27//!
28//! Each instance is created with a fuel budget
29//! ([`PluginLimits::fuel`]). wasmtime consumes fuel on every operation;
30//! when the budget is exhausted, the plugin traps with an out-of-fuel
31//! error, which the host catches and converts to a 500 (the plugin is
32//! misbehaving, not the request). Epoch-based interruption is used for
33//! time caps: a background thread increments the epoch, and the plugin
34//! is interrupted if it runs past [`PluginLimits::timeout_ms`].
35//!
36//! ## Memory caps
37//!
38//! [`PluginLimits::memory_mb`] caps the linear memory the plugin can
39//! allocate. wasmtime's `ResourceLimiter` trait enforces this at the
40//! allocation boundary.
41//!
42//! ## Import names and WASI
43//!
44//! Modules built with the Rust `proxy-wasm` SDK (what
45//! `dwara-cli plugin new` scaffolds) import the spec ABI names, so the
46//! host registers BOTH the spec names (`proxy_send_local_response`,
47//! `proxy_get_current_time_nanoseconds`) and dwara's original spellings
48//! (`proxy_send_http_response`, `proxy_get_current_time`) with
49//! identical behavior. The `wasm32-wasip1` target additionally emits a
50//! small `wasi_snapshot_preview1` import set (environment, fd_write,
51//! proc_exit, random_get, clock_time_get, sched_yield); the host
52//! provides minimal stubs for exactly that set — a module importing
53//! other WASI functions fails to instantiate (fail-closed).
54
55use std::collections::HashMap;
56use std::sync::{Arc, Mutex};
57
58use wasmtime::{Engine, Linker, Module, ResourceLimiter, Store};
59
60use super::abi;
61use super::callout::CalloutResponse;
62
63/// The VM-scoped shared-data store (DW-167): one map per compiled
64/// plugin module, shared by every per-request instance through the
65/// Mutex. Keys are strings; values are bytes with a CAS version for
66/// optimistic concurrency.
67pub type SharedData = HashMap<String, (Vec<u8>, u32)>;
68
69/// Per-module cap on the VM-scoped shared-data store: total bytes
70/// (key + value lengths summed over the entries). The store outlives
71/// every request (module lifetime), so an unbounded map is a slow OOM
72/// the wasm `ResourceLimiter` cannot see. A set that would exceed the
73/// cap FAILS (the plugin gets an error status it can branch on) —
74/// entries are never evicted: a silently evicted rate-limit counter
75/// or cache entry is a correctness bug, a refused set is a signal.
76pub const SHARED_DATA_CAP_BYTES: usize = 1024 * 1024;
77/// Per-module cap on the VM-scoped shared-data store: entry count
78/// (same posture as [`SHARED_DATA_CAP_BYTES`]; bounds key overhead a
79/// byte cap alone would miss).
80pub const SHARED_DATA_CAP_ENTRIES: usize = 1024;
81
82/// Whether inserting `key` -> `value` (replacing the current value if
83/// any) fits the shared-data caps ([`SHARED_DATA_CAP_BYTES`] /
84/// [`SHARED_DATA_CAP_ENTRIES`]). Runs inside the caller's critical
85/// section; the entry cap bounds the byte-sum walk to 1024 entries.
86fn shared_data_fits(shared: &SharedData, key: &str, value: &[u8]) -> bool {
87    let exists = shared.contains_key(key);
88    if !exists && shared.len() >= SHARED_DATA_CAP_ENTRIES {
89        return false;
90    }
91    let occupied: usize = shared.iter().map(|(k, (v, _))| k.len() + v.len()).sum();
92    let replaced = shared
93        .get(key)
94        .map(|(v, _)| key.len() + v.len())
95        .unwrap_or(0);
96    occupied - replaced + key.len() + value.len() <= SHARED_DATA_CAP_BYTES
97}
98
99/// Per-instance cap on buffered plugin log output (`proxy_log` plus
100/// the `fd_write` sink), in bytes. Fuel bounds plugin EXECUTION but
101/// not host-side copies: without this cap a plugin looping over
102/// `proxy_log`/`fd_write` would grow the host-side buffer without
103/// bound. When the cap is hit the head of the overflowing line is
104/// kept (if any budget remains), a marker line is appended, and every
105/// further line is dropped. 64 KiB is far above any legitimate
106/// diagnostic output.
107const LOG_BUFFER_CAP_BYTES: usize = 64 * 1024;
108
109/// Process-wide wasmtime engine + linker for proxy-wasm plugins.
110///
111/// Created once at startup and shared across all plugin instances. The
112/// engine is configured with fuel consumption and epoch interruption
113/// enabled. The linker connects the proxy-wasm ABI imports to the host
114/// functions.
115///
116/// Cheap to clone (Arc internals).
117#[derive(Clone)]
118pub struct WasmEngine {
119    engine: Engine,
120    linker: Arc<Linker<PluginContext>>,
121}
122
123/// A compiled plugin module (one per config entry).
124pub struct PluginModule {
125    module: Module,
126    limits: PluginLimits,
127    plugin_config: Vec<u8>,
128    vm_config: Vec<u8>,
129    /// The VM-scoped shared-data store (DW-167): every per-request
130    /// instance of THIS module sees the same map — the proxy-wasm
131    /// `proxy_get/set_shared_data` contract Envoy implements (shared
132    /// across the contexts of one VM). Instances on different tokio
133    /// tasks share it through the Mutex.
134    shared: Arc<Mutex<SharedData>>,
135}
136
137/// Per-plugin resource limits (DW-055 decision 4; §9.3).
138#[derive(Clone, Debug)]
139pub struct PluginLimits {
140    /// Maximum fuel (wasmtime operations). Each wasm operation consumes
141    /// a small amount of fuel; when the budget is exhausted, the plugin
142    /// traps. Default: 1,000,000 (enough for typical header inspection
143    /// and response short-circuiting).
144    pub fuel: u64,
145    /// Maximum linear memory in MB. Default: 32.
146    pub memory_mb: usize,
147    /// Maximum execution time in milliseconds. Enforced via epoch
148    /// interruption. Default: 100.
149    pub timeout_ms: u64,
150}
151
152impl Default for PluginLimits {
153    fn default() -> Self {
154        Self {
155            fuel: 1_000_000,
156            memory_mb: 32,
157            timeout_ms: 100,
158        }
159    }
160}
161
162/// The per-instance context the host imports read from and write to.
163///
164/// Stored as the wasmtime store's `T` type. The host import functions
165/// receive a `&mut PluginContext` via `Caller` and use it to exchange
166/// data with the plugin.
167pub struct PluginContext {
168    /// Request headers (set by the host before `proxy_on_request_headers`).
169    pub request_headers: Vec<(String, String)>,
170    /// Response headers (set by the host before `proxy_on_response_headers`).
171    pub response_headers: Vec<(String, String)>,
172    /// Request body (set by the host before `proxy_on_request_body`).
173    pub request_body: Vec<u8>,
174    /// Response body (set by the host before `proxy_on_response_body`).
175    pub response_body: Vec<u8>,
176    /// Plugin configuration (passed to `proxy_on_configure`).
177    pub plugin_config: Vec<u8>,
178    /// VM configuration (passed to `proxy_on_vm_start`).
179    pub vm_config: Vec<u8>,
180    /// The action the plugin returned from the last phase callback.
181    pub action: u32,
182    /// Whether the plugin has called `proxy_send_http_response` (a
183    /// short-circuit response). When set, the host should stop
184    /// processing and return the stored response.
185    pub local_response: Option<LocalResponse>,
186    /// Log lines emitted by the plugin via `proxy_log`.
187    pub logs: Vec<(u32, String)>,
188    /// The VM-scoped shared-data store handle (see [`SharedData`]).
189    pub shared_data: Arc<Mutex<SharedData>>,
190    /// Callouts registered by `proxy_http_call` and not yet answered
191    /// (DW-167). One instance can queue several; the dispatch driver
192    /// performs them in FIFO order, one round each.
193    pub pending_callouts: Vec<PendingCallout>,
194    /// Monotonic callout-token counter (tokens are handed out by the
195    /// host and delivered back with `proxy_on_http_call_response`).
196    pub callout_token_counter: u32,
197    /// The last delivered callout response's header map (`:status`
198    /// plus ordinary headers, hop-by-hop stripped) — what MapType 6
199    /// reads return inside `proxy_on_http_call_response`.
200    pub callout_response_headers: Vec<(String, String)>,
201    /// The last delivered callout response's body — what BufferType 4
202    /// reads return inside `proxy_on_http_call_response`.
203    pub callout_response_body: Vec<u8>,
204    /// Metrics registered by the plugin.
205    pub metrics: HashMap<String, PluginMetric>,
206    /// The current context ID (set by the host before calling exports).
207    pub current_context_id: u32,
208    /// The effective context ID (set by `proxy_set_effective_context`).
209    pub effective_context_id: u32,
210    /// Whether `proxy_done` was called.
211    pub done: bool,
212    /// Memory limiter state (tracks current allocation against the cap).
213    pub memory_used: usize,
214    /// Memory cap in bytes.
215    pub memory_cap: usize,
216    /// Log bytes buffered so far (the buffer is capped at
217    /// [`LOG_BUFFER_CAP_BYTES`]; see [`PluginContext::push_log`]).
218    log_bytes: usize,
219    /// Whether the log cap was hit — every further line is dropped.
220    log_dropped: bool,
221}
222
223/// A local response set by the plugin via `proxy_send_http_response`.
224#[derive(Clone, Debug)]
225pub struct LocalResponse {
226    pub status: u16,
227    pub headers: Vec<(String, String)>,
228    pub body: Vec<u8>,
229}
230
231/// A plugin-registered metric.
232#[derive(Clone, Debug)]
233pub struct PluginMetric {
234    pub metric_type: PluginMetricType,
235    pub value: f64,
236}
237
238/// One `proxy_http_call` dispatch, parsed and validated at the
239/// hostcall boundary (DW-167). The instance queues it; the dispatch
240/// driver (the async plugin_dispatch boundary) performs the exchange
241/// and delivers the response back through
242/// [`PluginInstance::deliver_callout_response`].
243#[derive(Clone, Debug)]
244pub struct PendingCallout {
245    /// The token the host handed the plugin (returned with the
246    /// response; the SDK dispatcher routes by it).
247    pub token: u32,
248    /// The full `http(s)://host[:port]/path?query` URI string.
249    pub uri: String,
250    /// The request method (`:method`; GET when the dispatch map
251    /// carried none).
252    pub method: String,
253    /// Ordinary headers (pseudo-headers stripped; `:authority` folds
254    /// into the URI's authority when the map carried one).
255    pub headers: Vec<(String, String)>,
256    /// The request body (may be empty).
257    pub body: Vec<u8>,
258    /// The plugin-requested timeout in milliseconds (the unit the
259    /// Rust SDK's `dispatch_http_call` sends); clamped at perform
260    /// time.
261    pub timeout_ms: u32,
262}
263
264/// Metric types (proxy-wasm §2.4).
265#[derive(Clone, Debug, PartialEq, Eq)]
266pub enum PluginMetricType {
267    Counter,
268    Gauge,
269    Histogram,
270}
271
272impl PluginContext {
273    fn new(
274        plugin_config: Vec<u8>,
275        vm_config: Vec<u8>,
276        memory_cap: usize,
277        shared: Arc<Mutex<SharedData>>,
278    ) -> Self {
279        Self {
280            request_headers: Vec::new(),
281            response_headers: Vec::new(),
282            request_body: Vec::new(),
283            response_body: Vec::new(),
284            plugin_config,
285            vm_config,
286            action: abi::ACTION_CONTINUE,
287            local_response: None,
288            logs: Vec::new(),
289            shared_data: shared,
290            pending_callouts: Vec::new(),
291            callout_token_counter: 0,
292            callout_response_headers: Vec::new(),
293            callout_response_body: Vec::new(),
294            metrics: HashMap::new(),
295            current_context_id: 0,
296            effective_context_id: 0,
297            done: false,
298            memory_used: 0,
299            memory_cap,
300            log_bytes: 0,
301            log_dropped: false,
302        }
303    }
304
305    /// Append a log line to the per-instance buffer, enforcing the
306    /// [`LOG_BUFFER_CAP_BYTES`] cap: once the budget is spent, the
307    /// head of the overflowing line is kept (if it fits), a truncation
308    /// marker is appended, and every further line is dropped. Use this
309    /// instead of pushing to `logs` directly.
310    fn push_log(&mut self, level: u32, msg: String) {
311        if self.log_dropped {
312            return;
313        }
314        if self.log_bytes + msg.len() > LOG_BUFFER_CAP_BYTES {
315            let remaining = LOG_BUFFER_CAP_BYTES - self.log_bytes;
316            if remaining > 0 {
317                // Keep the head of the overflowing line (a panic's
318                // first line is the diagnostic that matters).
319                let mut cut = remaining.min(msg.len());
320                while cut > 0 && !msg.is_char_boundary(cut) {
321                    cut -= 1;
322                }
323                if cut > 0 {
324                    self.log_bytes += cut;
325                    let head = msg[..cut].to_string();
326                    self.logs.push((level, head));
327                }
328            }
329            self.log_dropped = true;
330            self.logs.push((
331                level,
332                format!(
333                    "... log buffer cap ({} KiB) reached; further plugin log output dropped",
334                    LOG_BUFFER_CAP_BYTES / 1024
335                ),
336            ));
337            return;
338        }
339        self.log_bytes += msg.len();
340        self.logs.push((level, msg));
341    }
342}
343
344/// The memory cap for a plugin instance, enforced via wasmtime's
345/// `ResourceLimiter` trait on the store's user data (`PluginContext`).
346impl ResourceLimiter for PluginContext {
347    fn memory_growing(
348        &mut self,
349        _current: usize,
350        desired: usize,
351        _maximum: Option<usize>,
352    ) -> wasmtime::Result<bool> {
353        Ok(desired <= self.memory_cap)
354    }
355
356    fn table_growing(
357        &mut self,
358        _current: usize,
359        desired: usize,
360        _maximum: Option<usize>,
361    ) -> wasmtime::Result<bool> {
362        Ok(desired <= 10_000)
363    }
364}
365
366impl WasmEngine {
367    /// Create a new engine with the proxy-wasm ABI linker.
368    pub fn new() -> Result<Self, String> {
369        let mut config = wasmtime::Config::new();
370        config.consume_fuel(true);
371        config.epoch_interruption(true);
372        // cranelift is the compiler (enabled via the `cranelift` feature).
373        config.strategy(wasmtime::Strategy::Cranelift);
374
375        let engine = Engine::new(&config).map_err(|e| format!("wasmtime engine: {e}"))?;
376
377        let mut linker: Linker<PluginContext> = Linker::new(&engine);
378
379        // --- proxy-wasm ABI imports ---
380        //
381        // Each import is a function the plugin can call. The names
382        // follow the proxy-wasm spec (§3). All return i32 (WasmResult:
383        // 0 = OK, non-zero = error).
384
385        // proxy_log(level: i32, msg_ptr: i32, msg_size: i32) -> i32
386        linker
387            .func_wrap(
388                "env",
389                "proxy_log",
390                |mut caller: wasmtime::Caller<PluginContext>,
391                 level: i32,
392                 msg_ptr: i32,
393                 msg_size: i32|
394                 -> i32 {
395                    if msg_ptr < 0 || msg_size < 0 {
396                        return 1;
397                    }
398                    let memory = match caller.get_export("memory") {
399                        Some(wasmtime::Extern::Memory(m)) => m,
400                        _ => return 1,
401                    };
402                    let msg = match memory
403                        .data(&caller)
404                        .get(msg_ptr as usize..(msg_ptr as usize + msg_size as usize))
405                    {
406                        Some(slice) => slice.to_vec(),
407                        None => return 1,
408                    };
409                    let msg = String::from_utf8_lossy(&msg).into_owned();
410                    caller.data_mut().push_log(level as u32, msg);
411                    0
412                },
413            )
414            .map_err(|e| format!("linker proxy_log: {e}"))?;
415
416        // proxy_get_buffer_bytes(bt, offset, max_size, ptr_ptr, size_ptr) -> i32
417        linker
418            .func_wrap(
419                "env",
420                "proxy_get_buffer_bytes",
421                |mut caller: wasmtime::Caller<PluginContext>,
422                 bt: i32,
423                 offset: i32,
424                 max_size: i32,
425                 ptr_ptr: i32,
426                 size_ptr: i32|
427                 -> i32 {
428                    // Extract the requested chunk into an owned Vec so the
429                    // mutable borrow of `caller` ends before we touch memory.
430                    let chunk: Vec<u8> = {
431                        let ctx = caller.data_mut();
432                        let data = match bt as u32 {
433                            abi::BUFFER_REQUEST_BODY => &ctx.request_body,
434                            abi::BUFFER_RESPONSE_BODY => &ctx.response_body,
435                            abi::BUFFER_CALLOUT_RESPONSE_BODY => &ctx.callout_response_body,
436                            abi::BUFFER_PLUGIN_CONFIGURATION => &ctx.plugin_config,
437                            abi::BUFFER_VM_CONFIGURATION => &ctx.vm_config,
438                            _ => return 1,
439                        };
440                        let offset = offset as usize;
441                        let max_size = max_size as usize;
442                        if offset > data.len() {
443                            return 1;
444                        }
445                        let end = (offset + max_size).min(data.len());
446                        data[offset..end].to_vec()
447                    };
448                    let memory = match caller.get_export("memory") {
449                        Some(wasmtime::Extern::Memory(m)) => m,
450                        _ => return 1,
451                    };
452                    if chunk.is_empty() {
453                        // Write zero to ptr_ptr and 0 to size_ptr.
454                        if write_i32_to_memory(&memory, &mut caller, ptr_ptr, 0).is_err() {
455                            return 1;
456                        }
457                        if write_i32_to_memory(&memory, &mut caller, size_ptr, 0).is_err() {
458                            return 1;
459                        }
460                        return 0;
461                    }
462                    // Allocate space in plugin memory by calling the
463                    // plugin's `proxy_on_memory_allocate` export (the
464                    // standard proxy-wasm allocation pattern). If the
465                    // export doesn't exist, use a simple bump approach
466                    // via memory.grow.
467                    let alloc_ptr = match allocate_in_plugin(&mut caller, &memory, chunk.len()) {
468                        Ok(p) => p,
469                        Err(_) => return 1,
470                    };
471                    if memory
472                        .data_mut(&mut caller)
473                        .get_mut(alloc_ptr..alloc_ptr + chunk.len())
474                        .map(|dst| {
475                            dst.copy_from_slice(&chunk);
476                        })
477                        .is_none()
478                    {
479                        return 1;
480                    }
481                    if write_i32_to_memory(&memory, &mut caller, ptr_ptr, alloc_ptr as i32).is_err()
482                    {
483                        return 1;
484                    }
485                    if write_i32_to_memory(&memory, &mut caller, size_ptr, chunk.len() as i32)
486                        .is_err()
487                    {
488                        return 1;
489                    }
490                    0
491                },
492            )
493            .map_err(|e| format!("linker proxy_get_buffer_bytes: {e}"))?;
494
495        // proxy_get_buffer_status(bt, ptr_ptr, size_ptr) -> i32
496        linker
497            .func_wrap(
498                "env",
499                "proxy_get_buffer_status",
500                |mut caller: wasmtime::Caller<PluginContext>,
501                 bt: i32,
502                 _ptr_ptr: i32,
503                 size_ptr: i32|
504                 -> i32 {
505                    let ctx = caller.data();
506                    let len = match bt as u32 {
507                        abi::BUFFER_REQUEST_BODY => ctx.request_body.len(),
508                        abi::BUFFER_RESPONSE_BODY => ctx.response_body.len(),
509                        abi::BUFFER_CALLOUT_RESPONSE_BODY => ctx.callout_response_body.len(),
510                        abi::BUFFER_PLUGIN_CONFIGURATION => ctx.plugin_config.len(),
511                        abi::BUFFER_VM_CONFIGURATION => ctx.vm_config.len(),
512                        _ => return 1,
513                    };
514                    let memory = match caller.get_export("memory") {
515                        Some(wasmtime::Extern::Memory(m)) => m,
516                        _ => return 1,
517                    };
518                    if write_i32_to_memory(&memory, &mut caller, size_ptr, len as i32).is_err() {
519                        return 1;
520                    }
521                    0
522                },
523            )
524            .map_err(|e| format!("linker proxy_get_buffer_status: {e}"))?;
525
526        // proxy_set_buffer_bytes(bt, offset, size, ptr, size) -> i32
527        linker
528            .func_wrap(
529                "env",
530                "proxy_set_buffer_bytes",
531                |mut caller: wasmtime::Caller<PluginContext>,
532                 bt: i32,
533                 offset: i32,
534                 _size: i32,
535                 ptr: i32,
536                 data_size: i32|
537                 -> i32 {
538                    if ptr < 0 || data_size < 0 {
539                        return 1;
540                    }
541                    let memory = match caller.get_export("memory") {
542                        Some(wasmtime::Extern::Memory(m)) => m,
543                        _ => return 1,
544                    };
545                    let data = match memory
546                        .data(&caller)
547                        .get(ptr as usize..(ptr as usize + data_size as usize))
548                    {
549                        Some(slice) => slice.to_vec(),
550                        None => return 1,
551                    };
552                    let ctx = caller.data_mut();
553                    let offset = offset as usize;
554                    let buf = match bt as u32 {
555                        abi::BUFFER_REQUEST_BODY => &mut ctx.request_body,
556                        abi::BUFFER_RESPONSE_BODY => &mut ctx.response_body,
557                        _ => return 1,
558                    };
559                    if offset > buf.len() {
560                        buf.resize(offset, 0);
561                    }
562                    buf.splice(offset.., data);
563                    0
564                },
565            )
566            .map_err(|e| format!("linker proxy_set_buffer_bytes: {e}"))?;
567
568        // proxy_get_header_map_pairs(bt, ptr_ptr, size_ptr) -> i32
569        // Serialized in the spec/SDK wire layout (LE count + length
570        // table + NUL-terminated strings — what the Rust proxy-wasm
571        // SDK's get_map parses; see abi::serialize_header_map_spec).
572        // An empty map reports ptr=0/size=0 (the SDK reads that as an
573        // empty map without allocating).
574        linker
575            .func_wrap(
576                "env",
577                "proxy_get_header_map_pairs",
578                |mut caller: wasmtime::Caller<PluginContext>,
579                 bt: i32,
580                 ptr_ptr: i32,
581                 size_ptr: i32|
582                 -> i32 {
583                    let headers = {
584                        let ctx = caller.data();
585                        match bt as u32 {
586                            abi::BUFFER_REQUEST_HEADERS => ctx.request_headers.clone(),
587                            abi::BUFFER_RESPONSE_HEADERS => ctx.response_headers.clone(),
588                            // The delivered callout response's headers
589                            // (DW-167): `:status` first, then the
590                            // ordinary headers (hop-by-hop stripped).
591                            abi::BUFFER_CALLOUT_RESPONSE_HEADERS => {
592                                ctx.callout_response_headers.clone()
593                            }
594                            // Trailers are not plumbed through dwara's
595                            // pipeline: they read as an empty map.
596                            abi::BUFFER_REQUEST_TRAILERS
597                            | abi::BUFFER_RESPONSE_TRAILERS
598                            | abi::BUFFER_CALLOUT_RESPONSE_TRAILERS => Vec::new(),
599                            _ => return 1,
600                        }
601                    };
602                    let encoded = abi::serialize_header_map_spec(&headers);
603                    let memory = match caller.get_export("memory") {
604                        Some(wasmtime::Extern::Memory(m)) => m,
605                        _ => return 1,
606                    };
607                    if encoded.len() <= 4 {
608                        if write_i32_to_memory(&memory, &mut caller, ptr_ptr, 0).is_err() {
609                            return 1;
610                        }
611                        if write_i32_to_memory(&memory, &mut caller, size_ptr, 0).is_err() {
612                            return 1;
613                        }
614                        return 0;
615                    }
616                    let alloc_ptr = match allocate_in_plugin(&mut caller, &memory, encoded.len()) {
617                        Ok(p) => p,
618                        Err(_) => return 1,
619                    };
620                    if memory
621                        .data_mut(&mut caller)
622                        .get_mut(alloc_ptr..alloc_ptr + encoded.len())
623                        .map(|dst| dst.copy_from_slice(&encoded))
624                        .is_none()
625                    {
626                        return 1;
627                    }
628                    if write_i32_to_memory(&memory, &mut caller, ptr_ptr, alloc_ptr as i32).is_err()
629                    {
630                        return 1;
631                    }
632                    if write_i32_to_memory(&memory, &mut caller, size_ptr, encoded.len() as i32)
633                        .is_err()
634                    {
635                        return 1;
636                    }
637                    0
638                },
639            )
640            .map_err(|e| format!("linker proxy_get_header_map_pairs: {e}"))?;
641
642        // proxy_set_header_map_pairs(bt, ptr, size) -> i32
643        // Parses the spec/SDK wire layout (what the Rust proxy-wasm
644        // SDK's set_map serializes; see
645        // abi::deserialize_header_map_spec). A zero-size buffer is an
646        // empty map.
647        linker
648            .func_wrap(
649                "env",
650                "proxy_set_header_map_pairs",
651                |mut caller: wasmtime::Caller<PluginContext>,
652                 bt: i32,
653                 ptr: i32,
654                 size: i32|
655                 -> i32 {
656                    if ptr < 0 || size < 0 {
657                        return 1;
658                    }
659                    let memory = match caller.get_export("memory") {
660                        Some(wasmtime::Extern::Memory(m)) => m,
661                        _ => return 1,
662                    };
663                    let data = match memory
664                        .data(&caller)
665                        .get(ptr as usize..(ptr as usize + size as usize))
666                    {
667                        Some(slice) => slice.to_vec(),
668                        None => return 1,
669                    };
670                    let headers = match abi::deserialize_header_map_spec(&data) {
671                        Some(h) => h,
672                        None => return 1,
673                    };
674                    let ctx = caller.data_mut();
675                    match bt as u32 {
676                        abi::BUFFER_REQUEST_HEADERS => ctx.request_headers = headers,
677                        abi::BUFFER_RESPONSE_HEADERS => ctx.response_headers = headers,
678                        // Trailer and callout-response-map stores are
679                        // accepted and discarded (neither is plumbed
680                        // through the pipeline); reporting success
681                        // keeps SDK plugins from panicking on a
682                        // benign store.
683                        abi::BUFFER_REQUEST_TRAILERS
684                        | abi::BUFFER_RESPONSE_TRAILERS
685                        | abi::BUFFER_CALLOUT_RESPONSE_HEADERS
686                        | abi::BUFFER_CALLOUT_RESPONSE_TRAILERS => {}
687                        _ => return 1,
688                    }
689                    0
690                },
691            )
692            .map_err(|e| format!("linker proxy_set_header_map_pairs: {e}"))?;
693
694        // proxy_get_header_map_value(bt, key_ptr, key_size, value_ptr_ptr, value_size_ptr) -> i32
695        linker
696            .func_wrap(
697                "env",
698                "proxy_get_header_map_value",
699                |mut caller: wasmtime::Caller<PluginContext>,
700                 bt: i32,
701                 key_ptr: i32,
702                 key_size: i32,
703                 value_ptr_ptr: i32,
704                 value_size_ptr: i32|
705                 -> i32 {
706                    if key_ptr < 0 || key_size < 0 {
707                        return 1;
708                    }
709                    let memory = match caller.get_export("memory") {
710                        Some(wasmtime::Extern::Memory(m)) => m,
711                        _ => return 1,
712                    };
713                    let key = match memory
714                        .data(&caller)
715                        .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
716                    {
717                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
718                        None => return 1,
719                    };
720                    let value = {
721                        let ctx = caller.data();
722                        let headers: &[(String, String)] = match bt as u32 {
723                            abi::BUFFER_REQUEST_HEADERS => &ctx.request_headers,
724                            abi::BUFFER_RESPONSE_HEADERS => &ctx.response_headers,
725                            // The delivered callout response's headers
726                            // (DW-167): a lookup lands on `:status` or
727                            // an ordinary header the target sent.
728                            abi::BUFFER_CALLOUT_RESPONSE_HEADERS => &ctx.callout_response_headers,
729                            // Trailers are not plumbed through dwara's
730                            // pipeline: they read as empty (the SDK
731                            // maps an empty result to None).
732                            abi::BUFFER_REQUEST_TRAILERS
733                            | abi::BUFFER_RESPONSE_TRAILERS
734                            | abi::BUFFER_CALLOUT_RESPONSE_TRAILERS => &[],
735                            _ => return 1,
736                        };
737                        headers
738                            .iter()
739                            .find(|(k, _)| k.eq_ignore_ascii_case(&key))
740                            .map(|(_, v)| v.clone())
741                            .unwrap_or_default()
742                    };
743                    let memory = match caller.get_export("memory") {
744                        Some(wasmtime::Extern::Memory(m)) => m,
745                        _ => return 1,
746                    };
747                    if value.is_empty() {
748                        if write_i32_to_memory(&memory, &mut caller, value_ptr_ptr, 0).is_err() {
749                            return 1;
750                        }
751                        if write_i32_to_memory(&memory, &mut caller, value_size_ptr, 0).is_err() {
752                            return 1;
753                        }
754                        return 0;
755                    }
756                    let alloc_ptr = match allocate_in_plugin(&mut caller, &memory, value.len()) {
757                        Ok(p) => p,
758                        Err(_) => return 1,
759                    };
760                    if memory
761                        .data_mut(&mut caller)
762                        .get_mut(alloc_ptr..alloc_ptr + value.len())
763                        .map(|dst| dst.copy_from_slice(value.as_bytes()))
764                        .is_none()
765                    {
766                        return 1;
767                    }
768                    if write_i32_to_memory(&memory, &mut caller, value_ptr_ptr, alloc_ptr as i32)
769                        .is_err()
770                    {
771                        return 1;
772                    }
773                    if write_i32_to_memory(&memory, &mut caller, value_size_ptr, value.len() as i32)
774                        .is_err()
775                    {
776                        return 1;
777                    }
778                    0
779                },
780            )
781            .map_err(|e| format!("linker proxy_get_header_map_value: {e}"))?;
782
783        // proxy_add_header_map_value(bt, key_ptr, key_size, value_ptr, value_size) -> i32
784        linker
785            .func_wrap(
786                "env",
787                "proxy_add_header_map_value",
788                |mut caller: wasmtime::Caller<PluginContext>,
789                 bt: i32,
790                 key_ptr: i32,
791                 key_size: i32,
792                 value_ptr: i32,
793                 value_size: i32|
794                 -> i32 {
795                    if key_ptr < 0 || key_size < 0 || value_ptr < 0 || value_size < 0 {
796                        return 1;
797                    }
798                    let memory = match caller.get_export("memory") {
799                        Some(wasmtime::Extern::Memory(m)) => m,
800                        _ => return 1,
801                    };
802                    let key = match memory
803                        .data(&caller)
804                        .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
805                    {
806                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
807                        None => return 1,
808                    };
809                    let value = match memory
810                        .data(&caller)
811                        .get(value_ptr as usize..(value_ptr as usize + value_size as usize))
812                    {
813                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
814                        None => return 1,
815                    };
816                    let ctx = caller.data_mut();
817                    // Trailer and callout-response-map mutations are
818                    // accepted and discarded (neither is plumbed
819                    // through the pipeline); reporting success keeps
820                    // SDK plugins from panicking on a benign store.
821                    if bt as u32 == abi::BUFFER_REQUEST_TRAILERS
822                        || bt as u32 == abi::BUFFER_RESPONSE_TRAILERS
823                        || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_HEADERS
824                        || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_TRAILERS
825                    {
826                        return 0;
827                    }
828                    let headers = match bt as u32 {
829                        abi::BUFFER_REQUEST_HEADERS => &mut ctx.request_headers,
830                        abi::BUFFER_RESPONSE_HEADERS => &mut ctx.response_headers,
831                        _ => return 1,
832                    };
833                    headers.push((key, value));
834                    0
835                },
836            )
837            .map_err(|e| format!("linker proxy_add_header_map_value: {e}"))?;
838
839        // proxy_replace_header_map_value(bt, key_ptr, key_size, value_ptr, value_size) -> i32
840        linker
841            .func_wrap(
842                "env",
843                "proxy_replace_header_map_value",
844                |mut caller: wasmtime::Caller<PluginContext>,
845                 bt: i32,
846                 key_ptr: i32,
847                 key_size: i32,
848                 value_ptr: i32,
849                 value_size: i32|
850                 -> i32 {
851                    if key_ptr < 0 || key_size < 0 || value_ptr < 0 || value_size < 0 {
852                        return 1;
853                    }
854                    let memory = match caller.get_export("memory") {
855                        Some(wasmtime::Extern::Memory(m)) => m,
856                        _ => return 1,
857                    };
858                    let key = match memory
859                        .data(&caller)
860                        .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
861                    {
862                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
863                        None => return 1,
864                    };
865                    let value = match memory
866                        .data(&caller)
867                        .get(value_ptr as usize..(value_ptr as usize + value_size as usize))
868                    {
869                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
870                        None => return 1,
871                    };
872                    let ctx = caller.data_mut();
873                    // Trailer and callout-response-map mutations are
874                    // accepted and discarded (neither is plumbed
875                    // through the pipeline); reporting success keeps
876                    // SDK plugins from panicking on a benign store.
877                    if bt as u32 == abi::BUFFER_REQUEST_TRAILERS
878                        || bt as u32 == abi::BUFFER_RESPONSE_TRAILERS
879                        || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_HEADERS
880                        || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_TRAILERS
881                    {
882                        return 0;
883                    }
884                    let headers = match bt as u32 {
885                        abi::BUFFER_REQUEST_HEADERS => &mut ctx.request_headers,
886                        abi::BUFFER_RESPONSE_HEADERS => &mut ctx.response_headers,
887                        _ => return 1,
888                    };
889                    if let Some(entry) = headers
890                        .iter_mut()
891                        .find(|(k, _)| k.eq_ignore_ascii_case(&key))
892                    {
893                        entry.1 = value;
894                    } else {
895                        headers.push((key, value));
896                    }
897                    0
898                },
899            )
900            .map_err(|e| format!("linker proxy_replace_header_map_value: {e}"))?;
901
902        // proxy_remove_header_map_value(bt, key_ptr, key_size) -> i32
903        linker
904            .func_wrap(
905                "env",
906                "proxy_remove_header_map_value",
907                |mut caller: wasmtime::Caller<PluginContext>,
908                 bt: i32,
909                 key_ptr: i32,
910                 key_size: i32|
911                 -> i32 {
912                    if key_ptr < 0 || key_size < 0 {
913                        return 1;
914                    }
915                    let memory = match caller.get_export("memory") {
916                        Some(wasmtime::Extern::Memory(m)) => m,
917                        _ => return 1,
918                    };
919                    let key = match memory
920                        .data(&caller)
921                        .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
922                    {
923                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
924                        None => return 1,
925                    };
926                    let ctx = caller.data_mut();
927                    // Trailer and callout-response-map mutations are
928                    // accepted and discarded (neither is plumbed
929                    // through the pipeline); reporting success keeps
930                    // SDK plugins from panicking on a benign store.
931                    if bt as u32 == abi::BUFFER_REQUEST_TRAILERS
932                        || bt as u32 == abi::BUFFER_RESPONSE_TRAILERS
933                        || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_HEADERS
934                        || bt as u32 == abi::BUFFER_CALLOUT_RESPONSE_TRAILERS
935                    {
936                        return 0;
937                    }
938                    let headers = match bt as u32 {
939                        abi::BUFFER_REQUEST_HEADERS => &mut ctx.request_headers,
940                        abi::BUFFER_RESPONSE_HEADERS => &mut ctx.response_headers,
941                        _ => return 1,
942                    };
943                    headers.retain(|(k, _)| !k.eq_ignore_ascii_case(&key));
944                    0
945                },
946            )
947            .map_err(|e| format!("linker proxy_remove_header_map_value: {e}"))?;
948
949        // proxy_send_http_response(status, headers_ptr, headers_size, body_ptr, body_size, trailers_ptr, trailers_size) -> i32
950        // dwara's original spelling (the WAT fixtures and host tests
951        // use it).
952        linker
953            .func_wrap("env", "proxy_send_http_response", send_http_response_impl)
954            .map_err(|e| format!("linker proxy_send_http_response: {e}"))?;
955
956        // proxy_send_local_response(status_code, details_ptr, details_size,
957        //                            body_ptr, body_size, headers_ptr,
958        //                            headers_size, grpc_status) -> i32
959        // The proxy-wasm SPEC hostcall (a different signature and
960        // argument order than dwara's original spelling above): the
961        // Rust proxy-wasm SDK's `send_http_response` imports this name
962        // with this shape, and its header argument is always the
963        // spec/SDK map serialization (serialize_map — for empty
964        // headers that is the 4-byte LE count 0, so a 4-byte buffer
965        // parses as an empty map, never a parse failure). Status-code
966        // details and the gRPC status are carried on the
967        // LocalResponse's status/body path only — dwara does not
968        // forward them anywhere today.
969        linker
970            .func_wrap(
971                "env",
972                "proxy_send_local_response",
973                |mut caller: wasmtime::Caller<PluginContext>,
974                 status_code: i32,
975                 _details_ptr: i32,
976                 _details_size: i32,
977                 body_ptr: i32,
978                 body_size: i32,
979                 headers_ptr: i32,
980                 headers_size: i32,
981                 _grpc_status: i32|
982                 -> i32 {
983                    let memory = match caller.get_export("memory") {
984                        Some(wasmtime::Extern::Memory(m)) => m,
985                        _ => return 1,
986                    };
987                    let headers = if headers_ptr > 0 && headers_size > 0 {
988                        let data = match memory.data(&caller).get(
989                            headers_ptr as usize..(headers_ptr as usize + headers_size as usize),
990                        ) {
991                            Some(slice) => slice.to_vec(),
992                            None => return 1,
993                        };
994                        match abi::deserialize_header_map_spec(&data) {
995                            Some(h) => h,
996                            None => return 1,
997                        }
998                    } else {
999                        Vec::new()
1000                    };
1001                    let body = if body_ptr > 0 && body_size > 0 {
1002                        match memory
1003                            .data(&caller)
1004                            .get(body_ptr as usize..(body_ptr as usize + body_size as usize))
1005                        {
1006                            Some(slice) => slice.to_vec(),
1007                            None => return 1,
1008                        }
1009                    } else {
1010                        Vec::new()
1011                    };
1012                    let ctx = caller.data_mut();
1013                    ctx.local_response = Some(LocalResponse {
1014                        status: status_code as u16,
1015                        headers,
1016                        body,
1017                    });
1018                    ctx.action = abi::ACTION_END_STREAM;
1019                    0
1020                },
1021            )
1022            .map_err(|e| format!("linker proxy_send_local_response: {e}"))?;
1023
1024        // proxy_continue_stream(bt) -> i32
1025        linker
1026            .func_wrap(
1027                "env",
1028                "proxy_continue_stream",
1029                |_caller: wasmtime::Caller<PluginContext>, _bt: i32| -> i32 { 0 },
1030            )
1031            .map_err(|e| format!("linker proxy_continue_stream: {e}"))?;
1032
1033        // proxy_close_stream(bt) -> i32
1034        linker
1035            .func_wrap(
1036                "env",
1037                "proxy_close_stream",
1038                |mut caller: wasmtime::Caller<PluginContext>, _bt: i32| -> i32 {
1039                    caller.data_mut().action = abi::ACTION_END_STREAM;
1040                    0
1041                },
1042            )
1043            .map_err(|e| format!("linker proxy_close_stream: {e}"))?;
1044
1045        // proxy_get_shared_data(key_ptr, key_size, value_ptr_ptr, value_size_ptr, cas_ptr) -> i32
1046        linker
1047            .func_wrap(
1048                "env",
1049                "proxy_get_shared_data",
1050                |mut caller: wasmtime::Caller<PluginContext>,
1051                 key_ptr: i32,
1052                 key_size: i32,
1053                 value_ptr_ptr: i32,
1054                 value_size_ptr: i32,
1055                 cas_ptr: i32|
1056                 -> i32 {
1057                    if key_ptr < 0 || key_size < 0 {
1058                        return 1;
1059                    }
1060                    let memory = match caller.get_export("memory") {
1061                        Some(wasmtime::Extern::Memory(m)) => m,
1062                        _ => return 1,
1063                    };
1064                    let key = match memory
1065                        .data(&caller)
1066                        .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
1067                    {
1068                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1069                        None => return 1,
1070                    };
1071                    // VM-scoped store (DW-167): the lock is held only
1072                    // for the clone, never across a hostcall that
1073                    // re-enters wasm or writes plugin memory. Poison
1074                    // recovery: the critical section is a read+clone
1075                    // (nothing can half-mutate the map), so a panic
1076                    // elsewhere must not take every later request on
1077                    // this module down with the lock.
1078                    let lookup = {
1079                        let shared = caller
1080                            .data()
1081                            .shared_data
1082                            .lock()
1083                            .unwrap_or_else(std::sync::PoisonError::into_inner);
1084                        shared.get(&key).map(|(v, c)| (v.clone(), *c))
1085                    };
1086                    let Some((value, cas)) = lookup else {
1087                        // Not found: return OK with zero values.
1088                        if write_i32_to_memory(&memory, &mut caller, value_ptr_ptr, 0).is_err() {
1089                            return 1;
1090                        }
1091                        if write_i32_to_memory(&memory, &mut caller, value_size_ptr, 0).is_err() {
1092                            return 1;
1093                        }
1094                        if write_i32_to_memory(&memory, &mut caller, cas_ptr, 0).is_err() {
1095                            return 1;
1096                        }
1097                        return 0;
1098                    };
1099                    let alloc_ptr = match allocate_in_plugin(&mut caller, &memory, value.len()) {
1100                        Ok(p) => p,
1101                        Err(_) => return 1,
1102                    };
1103                    if memory
1104                        .data_mut(&mut caller)
1105                        .get_mut(alloc_ptr..alloc_ptr + value.len())
1106                        .map(|dst| dst.copy_from_slice(&value))
1107                        .is_none()
1108                    {
1109                        return 1;
1110                    }
1111                    if write_i32_to_memory(&memory, &mut caller, value_ptr_ptr, alloc_ptr as i32)
1112                        .is_err()
1113                    {
1114                        return 1;
1115                    }
1116                    if write_i32_to_memory(&memory, &mut caller, value_size_ptr, value.len() as i32)
1117                        .is_err()
1118                    {
1119                        return 1;
1120                    }
1121                    if write_i32_to_memory(&memory, &mut caller, cas_ptr, cas as i32).is_err() {
1122                        return 1;
1123                    }
1124                    0
1125                },
1126            )
1127            .map_err(|e| format!("linker proxy_get_shared_data: {e}"))?;
1128
1129        // proxy_set_shared_data(key_ptr, key_size, value_ptr, value_size, cas) -> i32
1130        linker
1131            .func_wrap(
1132                "env",
1133                "proxy_set_shared_data",
1134                |mut caller: wasmtime::Caller<PluginContext>,
1135                 key_ptr: i32,
1136                 key_size: i32,
1137                 value_ptr: i32,
1138                 value_size: i32,
1139                 cas: i32|
1140                 -> i32 {
1141                    if key_ptr < 0 || key_size < 0 {
1142                        return 1;
1143                    }
1144                    let memory = match caller.get_export("memory") {
1145                        Some(wasmtime::Extern::Memory(m)) => m,
1146                        _ => return 1,
1147                    };
1148                    let key = match memory
1149                        .data(&caller)
1150                        .get(key_ptr as usize..(key_ptr as usize + key_size as usize))
1151                    {
1152                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1153                        None => return 1,
1154                    };
1155                    let value = if value_ptr > 0 && value_size > 0 {
1156                        match memory
1157                            .data(&caller)
1158                            .get(value_ptr as usize..(value_ptr as usize + value_size as usize))
1159                        {
1160                            Some(slice) => slice.to_vec(),
1161                            None => return 1,
1162                        }
1163                    } else {
1164                        Vec::new()
1165                    };
1166                    let ctx = caller.data_mut();
1167                    // VM-scoped store (DW-167): the CAS read, the cap
1168                    // check, and the write are one critical section, so
1169                    // two instances racing a compare-and-set cannot
1170                    // interleave. Poison recovery: the section is a
1171                    // read plus one `insert` (the map cannot be left
1172                    // half-mutated), so a panic elsewhere must not
1173                    // poison every later request on this module.
1174                    let mut shared = ctx
1175                        .shared_data
1176                        .lock()
1177                        .unwrap_or_else(std::sync::PoisonError::into_inner);
1178                    let current_cas = shared.get(&key).map(|(_, c)| *c).unwrap_or(0);
1179                    if cas > 0 && cas != current_cas as i32 {
1180                        // Status::CasMismatch — the ONE error status
1181                        // the Rust SDK's set_shared_data maps to a
1182                        // clean Err (any other status panics the
1183                        // module).
1184                        return 8;
1185                    }
1186                    if !shared_data_fits(&shared, &key, &value) {
1187                        // Over-cap set: refused, never evicted (see
1188                        // SHARED_DATA_CAP_BYTES). The same error-status
1189                        // surface as the CAS mismatch — the SDK's only
1190                        // branchable error for this hostcall.
1191                        return 8;
1192                    }
1193                    let new_cas = current_cas + 1;
1194                    shared.insert(key, (value, new_cas));
1195                    0
1196                },
1197            )
1198            .map_err(|e| format!("linker proxy_set_shared_data: {e}"))?;
1199
1200        // proxy_set_effective_context(context_id) -> i32
1201        linker
1202            .func_wrap(
1203                "env",
1204                "proxy_set_effective_context",
1205                |mut caller: wasmtime::Caller<PluginContext>, context_id: i32| -> i32 {
1206                    caller.data_mut().effective_context_id = context_id as u32;
1207                    0
1208                },
1209            )
1210            .map_err(|e| format!("linker proxy_set_effective_context: {e}"))?;
1211
1212        // proxy_done() -> i32
1213        linker
1214            .func_wrap(
1215                "env",
1216                "proxy_done",
1217                |mut caller: wasmtime::Caller<PluginContext>| -> i32 {
1218                    caller.data_mut().done = true;
1219                    0
1220                },
1221            )
1222            .map_err(|e| format!("linker proxy_done: {e}"))?;
1223
1224        // proxy_get_property(path_ptr, path_size, value_ptr_ptr, value_size_ptr) -> i32
1225        // Minimal implementation: supports a few well-known properties.
1226        linker
1227            .func_wrap(
1228                "env",
1229                "proxy_get_property",
1230                |mut caller: wasmtime::Caller<PluginContext>,
1231                 path_ptr: i32,
1232                 path_size: i32,
1233                 value_ptr_ptr: i32,
1234                 value_size_ptr: i32|
1235                 -> i32 {
1236                    if path_ptr < 0 || path_size < 0 {
1237                        return 1;
1238                    }
1239                    let memory = match caller.get_export("memory") {
1240                        Some(wasmtime::Extern::Memory(m)) => m,
1241                        _ => return 1,
1242                    };
1243                    let path = match memory
1244                        .data(&caller)
1245                        .get(path_ptr as usize..(path_ptr as usize + path_size as usize))
1246                    {
1247                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1248                        None => return 1,
1249                    };
1250                    // Minimal property support: return empty for unknown
1251                    // properties (the plugin should handle this).
1252                    let _ = path;
1253                    if write_i32_to_memory(&memory, &mut caller, value_ptr_ptr, 0).is_err() {
1254                        return 1;
1255                    }
1256                    if write_i32_to_memory(&memory, &mut caller, value_size_ptr, 0).is_err() {
1257                        return 1;
1258                    }
1259                    0
1260                },
1261            )
1262            .map_err(|e| format!("linker proxy_get_property: {e}"))?;
1263
1264        // proxy_set_property(path_ptr, path_size, value_ptr, value_size) -> i32
1265        // Minimal implementation: no-op (returns OK).
1266        linker
1267            .func_wrap(
1268                "env",
1269                "proxy_set_property",
1270                |_caller: wasmtime::Caller<PluginContext>,
1271                 _path_ptr: i32,
1272                 _path_size: i32,
1273                 _value_ptr: i32,
1274                 _value_size: i32|
1275                 -> i32 { 0 },
1276            )
1277            .map_err(|e| format!("linker proxy_set_property: {e}"))?;
1278
1279        // proxy_define_metric(metric_type, name_ptr, name_size, metric_id_ptr, metric_id_size_ptr) -> i32
1280        linker
1281            .func_wrap(
1282                "env",
1283                "proxy_define_metric",
1284                |mut caller: wasmtime::Caller<PluginContext>,
1285                 metric_type: i32,
1286                 name_ptr: i32,
1287                 name_size: i32,
1288                 _metric_id_ptr: i32,
1289                 _metric_id_size_ptr: i32|
1290                 -> i32 {
1291                    if name_ptr < 0 || name_size < 0 {
1292                        return 1;
1293                    }
1294                    let memory = match caller.get_export("memory") {
1295                        Some(wasmtime::Extern::Memory(m)) => m,
1296                        _ => return 1,
1297                    };
1298                    let name = match memory
1299                        .data(&caller)
1300                        .get(name_ptr as usize..(name_ptr as usize + name_size as usize))
1301                    {
1302                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1303                        None => return 1,
1304                    };
1305                    let mt = match metric_type {
1306                        0 => PluginMetricType::Counter,
1307                        1 => PluginMetricType::Gauge,
1308                        2 => PluginMetricType::Histogram,
1309                        _ => return 1,
1310                    };
1311                    let ctx = caller.data_mut();
1312                    ctx.metrics.insert(
1313                        name,
1314                        PluginMetric {
1315                            metric_type: mt,
1316                            value: 0.0,
1317                        },
1318                    );
1319                    0
1320                },
1321            )
1322            .map_err(|e| format!("linker proxy_define_metric: {e}"))?;
1323
1324        // proxy_record_metric(metric_id_ptr, metric_id_size, value) -> i32
1325        linker
1326            .func_wrap(
1327                "env",
1328                "proxy_record_metric",
1329                |mut caller: wasmtime::Caller<PluginContext>,
1330                 name_ptr: i32,
1331                 name_size: i32,
1332                 value: f64|
1333                 -> i32 {
1334                    if name_ptr < 0 || name_size < 0 {
1335                        return 1;
1336                    }
1337                    let memory = match caller.get_export("memory") {
1338                        Some(wasmtime::Extern::Memory(m)) => m,
1339                        _ => return 1,
1340                    };
1341                    let name = match memory
1342                        .data(&caller)
1343                        .get(name_ptr as usize..(name_ptr as usize + name_size as usize))
1344                    {
1345                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1346                        None => return 1,
1347                    };
1348                    let ctx = caller.data_mut();
1349                    if let Some(m) = ctx.metrics.get_mut(&name) {
1350                        m.value = value;
1351                    }
1352                    0
1353                },
1354            )
1355            .map_err(|e| format!("linker proxy_record_metric: {e}"))?;
1356
1357        // proxy_increment_metric(metric_id_ptr, metric_id_size, increment) -> i32
1358        linker
1359            .func_wrap(
1360                "env",
1361                "proxy_increment_metric",
1362                |mut caller: wasmtime::Caller<PluginContext>,
1363                 name_ptr: i32,
1364                 name_size: i32,
1365                 increment: f64|
1366                 -> i32 {
1367                    if name_ptr < 0 || name_size < 0 {
1368                        return 1;
1369                    }
1370                    let memory = match caller.get_export("memory") {
1371                        Some(wasmtime::Extern::Memory(m)) => m,
1372                        _ => return 1,
1373                    };
1374                    let name = match memory
1375                        .data(&caller)
1376                        .get(name_ptr as usize..(name_ptr as usize + name_size as usize))
1377                    {
1378                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1379                        None => return 1,
1380                    };
1381                    let ctx = caller.data_mut();
1382                    if let Some(m) = ctx.metrics.get_mut(&name) {
1383                        m.value += increment;
1384                    }
1385                    0
1386                },
1387            )
1388            .map_err(|e| format!("linker proxy_increment_metric: {e}"))?;
1389
1390        // proxy_get_metric(metric_id_ptr, metric_id_size, return_value_ptr) -> i32
1391        linker
1392            .func_wrap(
1393                "env",
1394                "proxy_get_metric",
1395                |mut caller: wasmtime::Caller<PluginContext>,
1396                 name_ptr: i32,
1397                 name_size: i32,
1398                 return_value_ptr: i32|
1399                 -> i32 {
1400                    if name_ptr < 0 || name_size < 0 {
1401                        return 1;
1402                    }
1403                    let memory = match caller.get_export("memory") {
1404                        Some(wasmtime::Extern::Memory(m)) => m,
1405                        _ => return 1,
1406                    };
1407                    let name = match memory
1408                        .data(&caller)
1409                        .get(name_ptr as usize..(name_ptr as usize + name_size as usize))
1410                    {
1411                        Some(slice) => String::from_utf8_lossy(slice).into_owned(),
1412                        None => return 1,
1413                    };
1414                    let value = caller
1415                        .data()
1416                        .metrics
1417                        .get(&name)
1418                        .map(|m| m.value)
1419                        .unwrap_or(0.0);
1420                    // Write the f64 value to the return pointer.
1421                    let bytes = value.to_le_bytes();
1422                    if memory
1423                        .data_mut(&mut caller)
1424                        .get_mut(return_value_ptr as usize..(return_value_ptr as usize + 8))
1425                        .map(|dst| dst.copy_from_slice(&bytes))
1426                        .is_none()
1427                    {
1428                        return 1;
1429                    }
1430                    0
1431                },
1432            )
1433            .map_err(|e| format!("linker proxy_get_metric: {e}"))?;
1434
1435        // proxy_get_current_time(return_value_ptr) -> i32
1436        // proxy_get_current_time_nanoseconds(return_time) -> i32 (the
1437        // proxy-wasm spec name; the Rust SDK's `get_current_time`
1438        // imports this spelling). Both names share one implementation.
1439        for name in [
1440            "proxy_get_current_time",
1441            "proxy_get_current_time_nanoseconds",
1442        ] {
1443            linker
1444                .func_wrap("env", name, get_current_time_impl)
1445                .map_err(|e| format!("linker {name}: {e}"))?;
1446        }
1447
1448        // proxy_get_log_level(return_level_ptr) -> i32
1449        // The host buffers every proxy_log line (no level filtering),
1450        // so the query always reports Info. Registered because the Rust
1451        // SDK declares the import even when unused; plugins that gate
1452        // work on the reported level see Info.
1453        linker
1454            .func_wrap(
1455                "env",
1456                "proxy_get_log_level",
1457                |mut caller: wasmtime::Caller<PluginContext>, return_level_ptr: i32| -> i32 {
1458                    let memory = match caller.get_export("memory") {
1459                        Some(wasmtime::Extern::Memory(m)) => m,
1460                        _ => return 1,
1461                    };
1462                    if write_i32_to_memory(
1463                        &memory,
1464                        &mut caller,
1465                        return_level_ptr,
1466                        abi::LOG_INFO as i32,
1467                    )
1468                    .is_err()
1469                    {
1470                        return 1;
1471                    }
1472                    0
1473                },
1474            )
1475            .map_err(|e| format!("linker proxy_get_log_level: {e}"))?;
1476
1477        // proxy_get_status(return_code_ptr, return_message_ptr_ptr, return_message_size_ptr) -> i32
1478        // The gRPC callout status fetch (the Rust SDK's
1479        // `get_grpc_status`). dwara's grpc_* hostcalls never put a
1480        // callout in flight, so there is no status to report — but the
1481        // SDK 0.2.5 wrapper PANICS on any non-Ok status (hostcalls.rs
1482        // `get_grpc_status`, ~line 1037), so answering NotFound would
1483        // trap every SDK caller. Return Ok with code 0 and a null
1484        // message (the SDK reads that as `(0, None)`: no gRPC status).
1485        linker
1486            .func_wrap(
1487                "env",
1488                "proxy_get_status",
1489                |mut caller: wasmtime::Caller<PluginContext>,
1490                 return_code_ptr: i32,
1491                 return_message_ptr_ptr: i32,
1492                 return_message_size_ptr: i32|
1493                 -> i32 {
1494                    let memory = match caller.get_export("memory") {
1495                        Some(wasmtime::Extern::Memory(m)) => m,
1496                        _ => return 1,
1497                    };
1498                    for (ptr, value) in [
1499                        (return_code_ptr, 0),
1500                        (return_message_ptr_ptr, 0),
1501                        (return_message_size_ptr, 0),
1502                    ] {
1503                        if write_i32_to_memory(&memory, &mut caller, ptr, value).is_err() {
1504                            return 1;
1505                        }
1506                    }
1507                    0 // Ok: no callout, no gRPC status (code 0, null message)
1508                },
1509            )
1510            .map_err(|e| format!("linker proxy_get_status: {e}"))?;
1511
1512        // Stub imports for functions we don't implement but the plugin
1513        // may call (returns error to signal unsupported). Each is
1514        // registered with its proxy-wasm spec ARITY: wasmtime's typed
1515        // linking rejects a module whose import signature does not
1516        // match, and the Rust SDK declares every import with its spec
1517        // signature even when the plugin never calls it.
1518        for name in [
1519            "proxy_set_tick_period_milliseconds",
1520            "proxy_grpc_cancel",
1521            "proxy_grpc_close",
1522        ] {
1523            linker
1524                .func_wrap("env", name, stub_unsupported_1)
1525                .map_err(|e| format!("linker {name}: {e}"))?;
1526        }
1527        for name in [
1528            "proxy_register_shared_queue",
1529            "proxy_dequeue_shared_queue",
1530            "proxy_enqueue_shared_queue",
1531        ] {
1532            linker
1533                .func_wrap("env", name, stub_unsupported_3)
1534                .map_err(|e| format!("linker {name}: {e}"))?;
1535        }
1536        linker
1537            .func_wrap("env", "proxy_resolve_shared_queue", stub_unsupported_5)
1538            .map_err(|e| format!("linker proxy_resolve_shared_queue: {e}"))?;
1539        linker
1540            .func_wrap("env", "proxy_grpc_stream", stub_unsupported_9)
1541            .map_err(|e| format!("linker proxy_grpc_stream: {e}"))?;
1542        // proxy_http_call(upstream_ptr, upstream_size, headers_ptr,
1543        //                 headers_size, body_ptr, body_size,
1544        //                 trailers_ptr, trailers_size, timeout_ms,
1545        //                 return_token_ptr) -> i32
1546        // The proxy-wasm HTTP callout dispatcher (DW-167). The Rust
1547        // SDK's `dispatch_http_call` (hostcalls.rs ~812) sends the URI
1548        // string, the headers/trailers in the spec map serialization
1549        // (what `proxy_set_header_map_pairs` parses), an optional
1550        // body, and the timeout in MILLISECONDS; it reads the token
1551        // from the return pointer and maps Ok/BadArgument/
1552        // InternalFailure to `Ok(token)`/`Err(BadArgument)`/
1553        // `Err(InternalFailure)` — every other status panics the
1554        // module, so this import answers only 0/2/10.
1555        linker
1556            .func_wrap(
1557                "env",
1558                "proxy_http_call",
1559                |mut caller: wasmtime::Caller<PluginContext>,
1560                 upstream_ptr: i32,
1561                 upstream_size: i32,
1562                 headers_ptr: i32,
1563                 headers_size: i32,
1564                 body_ptr: i32,
1565                 body_size: i32,
1566                 _trailers_ptr: i32,
1567                 _trailers_size: i32,
1568                 timeout_ms: i32,
1569                 return_token_ptr: i32|
1570                 -> i32 {
1571                    if upstream_ptr < 0 || upstream_size < 0 || headers_ptr < 0 || headers_size < 0
1572                    {
1573                        return 2; // Status::BadArgument
1574                    }
1575                    let memory = match caller.get_export("memory") {
1576                        Some(wasmtime::Extern::Memory(m)) => m,
1577                        _ => return 10,
1578                    };
1579                    let read = |ptr: i32, size: i32| -> Option<Vec<u8>> {
1580                        memory
1581                            .data(&caller)
1582                            .get(ptr as usize..(ptr as usize + size as usize))
1583                            .map(|s| s.to_vec())
1584                    };
1585                    let uri_bytes = match read(upstream_ptr, upstream_size) {
1586                        Some(b) => b,
1587                        None => return 2,
1588                    };
1589                    let uri = match String::from_utf8(uri_bytes) {
1590                        Ok(u) => u,
1591                        Err(_) => return 2,
1592                    };
1593                    let headers_bytes = match read(headers_ptr, headers_size) {
1594                        Some(b) => b,
1595                        None => return 2,
1596                    };
1597                    let body = if body_ptr > 0 && body_size > 0 {
1598                        match read(body_ptr, body_size) {
1599                            Some(b) => b,
1600                            None => return 2,
1601                        }
1602                    } else {
1603                        Vec::new()
1604                    };
1605                    // Trailers are accepted and ignored (dwara does
1606                    // not plumb trailers on callout requests).
1607                    match register_callout(
1608                        caller.data_mut(),
1609                        &uri,
1610                        &headers_bytes,
1611                        body,
1612                        timeout_ms,
1613                    ) {
1614                        Some(token) => {
1615                            let memory = match caller.get_export("memory") {
1616                                Some(wasmtime::Extern::Memory(m)) => m,
1617                                _ => return 10,
1618                            };
1619                            if write_i32_to_memory(
1620                                &memory,
1621                                &mut caller,
1622                                return_token_ptr,
1623                                token as i32,
1624                            )
1625                            .is_err()
1626                            {
1627                                return 10;
1628                            }
1629                            0 // Status::Ok
1630                        }
1631                        None => 2, // Status::BadArgument (invalid URI/map/head)
1632                    }
1633                },
1634            )
1635            .map_err(|e| format!("linker proxy_http_call: {e}"))?;
1636        linker
1637            .func_wrap("env", "proxy_grpc_call", stub_unsupported_12)
1638            .map_err(|e| format!("linker proxy_grpc_call: {e}"))?;
1639        linker
1640            .func_wrap("env", "proxy_grpc_send", stub_unsupported_4)
1641            .map_err(|e| format!("linker proxy_grpc_send: {e}"))?;
1642        linker
1643            .func_wrap("env", "proxy_call_foreign_function", stub_unsupported_6)
1644            .map_err(|e| format!("linker proxy_call_foreign_function: {e}"))?;
1645
1646        // --- WASI p1 stubs (wasm32-wasip1) ----------------------------------
1647        //
1648        // Modules built for wasm32-wasip1 emit a small
1649        // `wasi_snapshot_preview1` import set even when they never
1650        // touch WASI consciously (Rust std's panic printing, HashMap
1651        // seeding, environment access). The host provides minimal
1652        // stubs for exactly that observed set; anything beyond it
1653        // (files, sockets) fails to instantiate, fail-closed.
1654
1655        // environ_sizes_get(environ_count_ptr, environ_buf_size_ptr) -> errno
1656        linker
1657            .func_wrap(
1658                "wasi_snapshot_preview1",
1659                "environ_sizes_get",
1660                |mut caller: wasmtime::Caller<PluginContext>,
1661                 count_ptr: i32,
1662                 buf_size_ptr: i32|
1663                 -> i32 {
1664                    let memory = match caller.get_export("memory") {
1665                        Some(wasmtime::Extern::Memory(m)) => m,
1666                        _ => return 1,
1667                    };
1668                    let _ = caller.data_mut();
1669                    // Empty environment: count 0, buffer size 0.
1670                    for (ptr, value) in [(count_ptr, 0), (buf_size_ptr, 0)] {
1671                        if write_i32_to_memory(&memory, &mut caller, ptr, value).is_err() {
1672                            return 1; // EPERM-ish; any non-zero is an errno
1673                        }
1674                    }
1675                    0 // ESUCCESS
1676                },
1677            )
1678            .map_err(|e| format!("linker environ_sizes_get: {e}"))?;
1679
1680        // environ_get(environ_ptrs_ptr, environ_buf_ptr) -> errno
1681        linker
1682            .func_wrap(
1683                "wasi_snapshot_preview1",
1684                "environ_get",
1685                |_caller: wasmtime::Caller<PluginContext>,
1686                 _environ_ptrs_ptr: i32,
1687                 _environ_buf_ptr: i32|
1688                 -> i32 {
1689                    0 // ESUCCESS: the (empty) environment needs no writes
1690                },
1691            )
1692            .map_err(|e| format!("linker environ_get: {e}"))?;
1693
1694        // fd_write(fd, iovs_ptr, iovs_len, nwritten_ptr) -> errno
1695        // Route the plugin's stderr (panic output, debug prints) into
1696        // the per-instance log buffer so operators can see why a plugin
1697        // died without the bytes escaping the sandbox. The buffer is
1698        // capped (PluginContext::push_log): a plugin that spews through
1699        // stderr cannot grow host memory without bound — unlike fuel,
1700        // which bounds plugin execution, not host-side copies.
1701        linker
1702            .func_wrap(
1703                "wasi_snapshot_preview1",
1704                "fd_write",
1705                |mut caller: wasmtime::Caller<PluginContext>,
1706                 _fd: i32,
1707                 iovs_ptr: i32,
1708                 iovs_len: i32,
1709                 nwritten_ptr: i32|
1710                 -> i32 {
1711                    let memory = match caller.get_export("memory") {
1712                        Some(wasmtime::Extern::Memory(m)) => m,
1713                        _ => return 1,
1714                    };
1715                    if iovs_ptr < 0 || iovs_len < 0 || nwritten_ptr < 0 {
1716                        return 1;
1717                    }
1718                    let mut out = Vec::new();
1719                    for i in 0..iovs_len as usize {
1720                        let iov_base = iovs_ptr as usize + i * 8;
1721                        let (buf_ptr, buf_len) = {
1722                            let data = memory.data(&caller);
1723                            let Some(base) = data.get(iov_base..iov_base + 8) else {
1724                                return 1;
1725                            };
1726                            let buf_ptr =
1727                                u32::from_le_bytes(base[0..4].try_into().expect("4 bytes"))
1728                                    as usize;
1729                            let buf_len =
1730                                u32::from_le_bytes(base[4..8].try_into().expect("4 bytes"))
1731                                    as usize;
1732                            (buf_ptr, buf_len)
1733                        };
1734                        match memory.data(&caller).get(buf_ptr..buf_ptr + buf_len) {
1735                            Some(slice) => out.extend_from_slice(slice),
1736                            None => return 1,
1737                        }
1738                    }
1739                    let written = out.len() as i32;
1740                    caller
1741                        .data_mut()
1742                        .push_log(abi::LOG_INFO, String::from_utf8_lossy(&out).into_owned());
1743                    if write_i32_to_memory(&memory, &mut caller, nwritten_ptr, written).is_err() {
1744                        return 1;
1745                    }
1746                    0 // ESUCCESS
1747                },
1748            )
1749            .map_err(|e| format!("linker fd_write: {e}"))?;
1750
1751        // proc_exit(code) -> ! : the plugin terminated itself. Trap the
1752        // instance (the runner reports the trap like any other plugin
1753        // failure; the code rides the trap message).
1754        linker
1755            .func_wrap(
1756                "wasi_snapshot_preview1",
1757                "proc_exit",
1758                |_caller: wasmtime::Caller<PluginContext>,
1759                 code: i32|
1760                 -> Result<(), wasmtime::Error> {
1761                    Err(wasmtime::format_err!(
1762                        "wasi proc_exit({code}): the plugin terminated itself"
1763                    ))
1764                },
1765            )
1766            .map_err(|e| format!("linker proc_exit: {e}"))?;
1767
1768        // random_get(buf_ptr, buf_len) -> errno
1769        // Non-cryptographic: splitmix64 seeded from the wall clock and
1770        // a process-global counter. Enough to seed std collections;
1771        // plugins must not use it for key material.
1772        linker
1773            .func_wrap(
1774                "wasi_snapshot_preview1",
1775                "random_get",
1776                |mut caller: wasmtime::Caller<PluginContext>, buf_ptr: i32, buf_len: i32| -> i32 {
1777                    if buf_ptr < 0 || buf_len < 0 {
1778                        return 1;
1779                    }
1780                    let memory = match caller.get_export("memory") {
1781                        Some(wasmtime::Extern::Memory(m)) => m,
1782                        _ => return 1,
1783                    };
1784                    let mut state = wasi_random_seed();
1785                    let mut filled = 0usize;
1786                    while filled < buf_len as usize {
1787                        state = state.wrapping_add(0x9E3779B97F4A7C15);
1788                        let mut z = state;
1789                        z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
1790                        z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
1791                        z ^= z >> 31;
1792                        for byte in z.to_le_bytes() {
1793                            if filled >= buf_len as usize {
1794                                break;
1795                            }
1796                            let dst = buf_ptr as usize + filled;
1797                            match memory.data_mut(&mut caller).get_mut(dst..dst + 1) {
1798                                Some(slot) => slot[0] = byte,
1799                                None => return 1,
1800                            }
1801                            filled += 1;
1802                        }
1803                    }
1804                    0 // ESUCCESS
1805                },
1806            )
1807            .map_err(|e| format!("linker random_get: {e}"))?;
1808
1809        // clock_time_get(clock_id, precision, time_ptr) -> errno
1810        linker
1811            .func_wrap(
1812                "wasi_snapshot_preview1",
1813                "clock_time_get",
1814                |mut caller: wasmtime::Caller<PluginContext>,
1815                 _clock_id: i32,
1816                 _precision: i64,
1817                 time_ptr: i32|
1818                 -> i32 {
1819                    let memory = match caller.get_export("memory") {
1820                        Some(wasmtime::Extern::Memory(m)) => m,
1821                        _ => return 1,
1822                    };
1823                    let now = std::time::SystemTime::now()
1824                        .duration_since(std::time::UNIX_EPOCH)
1825                        .map(|d| d.as_nanos() as u64)
1826                        .unwrap_or(0);
1827                    let bytes = now.to_le_bytes();
1828                    if memory
1829                        .data_mut(&mut caller)
1830                        .get_mut(time_ptr as usize..(time_ptr as usize + 8))
1831                        .map(|dst| dst.copy_from_slice(&bytes))
1832                        .is_none()
1833                    {
1834                        return 1;
1835                    }
1836                    0 // ESUCCESS
1837                },
1838            )
1839            .map_err(|e| format!("linker clock_time_get: {e}"))?;
1840
1841        // sched_yield() -> errno
1842        linker
1843            .func_wrap(
1844                "wasi_snapshot_preview1",
1845                "sched_yield",
1846                |_caller: wasmtime::Caller<PluginContext>| -> i32 { 0 },
1847            )
1848            .map_err(|e| format!("linker sched_yield: {e}"))?;
1849
1850        Ok(Self {
1851            engine,
1852            linker: Arc::new(linker),
1853        })
1854    }
1855
1856    /// The underlying wasmtime engine.
1857    pub fn engine(&self) -> &Engine {
1858        &self.engine
1859    }
1860
1861    /// Compile a .wasm module from bytes.
1862    pub fn compile(
1863        &self,
1864        wasm_bytes: &[u8],
1865        limits: PluginLimits,
1866        plugin_config: Vec<u8>,
1867        vm_config: Vec<u8>,
1868    ) -> Result<PluginModule, String> {
1869        let module =
1870            Module::new(&self.engine, wasm_bytes).map_err(|e| format!("wasm compile: {e}"))?;
1871        Ok(PluginModule {
1872            module,
1873            limits,
1874            plugin_config,
1875            vm_config,
1876            shared: Arc::new(Mutex::new(SharedData::new())),
1877        })
1878    }
1879
1880    /// The linker (used by instance creation).
1881    pub fn linker(&self) -> &Linker<PluginContext> {
1882        &self.linker
1883    }
1884}
1885
1886impl PluginModule {
1887    /// Create a new per-request instance and run the VM start + configure
1888    /// lifecycle. Returns the store and instance ready for phase calls.
1889    pub fn instantiate(&self, engine: &WasmEngine) -> Result<PluginInstance, String> {
1890        let memory_cap = self.limits.memory_mb * 1024 * 1024;
1891        let ctx = PluginContext::new(
1892            self.plugin_config.clone(),
1893            self.vm_config.clone(),
1894            memory_cap,
1895            Arc::clone(&self.shared),
1896        );
1897        let mut store = Store::new(&engine.engine, ctx);
1898        store.limiter(move |ctx| ctx as &mut dyn ResourceLimiter);
1899
1900        // Set the fuel budget.
1901        store
1902            .set_fuel(self.limits.fuel)
1903            .map_err(|e| format!("set_fuel: {e}"))?;
1904
1905        // Set the epoch deadline so epoch interruption does not fire
1906        // immediately. A background thread (when configured) increments
1907        // the engine epoch; the store traps when the current epoch
1908        // reaches the deadline. Without this call the default deadline
1909        // is 0 and any wasm execution traps right away.
1910        store.set_epoch_deadline(self.limits.timeout_ms.max(1));
1911
1912        // Instantiate the module.
1913        let instance = engine
1914            .linker
1915            .instantiate(&mut store, &self.module)
1916            .map_err(|e| format!("wasm instantiate: {e}"))?;
1917
1918        let mut inst = PluginInstance {
1919            store,
1920            instance,
1921            http_context_created: false,
1922        };
1923
1924        // Standard proxy-wasm VM initialization (SDK modules, e.g.
1925        // anything built from `dwara-cli plugin new`): the module
1926        // registers its root-context factory in `_start` and builds its
1927        // contexts on `proxy_on_context_create`; calling
1928        // `proxy_on_vm_start` without both panics inside the module.
1929        // Both exports are optional (dwara's WAT fixtures implement
1930        // neither) — call them when present, skip when absent. A trap
1931        // inside `_start` fails the instantiation with the trap itself.
1932        inst.call_optional_start()?;
1933        inst.call_optional_context_create(1, 0);
1934
1935        // Call proxy_on_vm_start(root_context_id=1, vm_config_size).
1936        inst.set_context_id(1);
1937        if let Some(export) = inst
1938            .instance
1939            .get_export(&mut inst.store, "proxy_on_vm_start")
1940        {
1941            let func = export
1942                .into_func()
1943                .ok_or("proxy_on_vm_start is not a func")?;
1944            let vm_config_size = self.vm_config.len() as i32;
1945            let typed: wasmtime::TypedFunc<(i32, i32), i32> = func
1946                .typed(&inst.store)
1947                .map_err(|e| format!("proxy_on_vm_start typed: {e}"))?;
1948            let result = typed
1949                .call(&mut inst.store, (1, vm_config_size))
1950                .map_err(|e| format!("proxy_on_vm_start call: {e}"))?;
1951            if result == 0 {
1952                return Err("proxy_on_vm_start returned false".to_string());
1953            }
1954        }
1955
1956        // Call proxy_on_configure(root_context_id=1, plugin_config_size).
1957        if let Some(export) = inst
1958            .instance
1959            .get_export(&mut inst.store, "proxy_on_configure")
1960        {
1961            let func = export
1962                .into_func()
1963                .ok_or("proxy_on_configure is not a func")?;
1964            let plugin_config_size = self.plugin_config.len() as i32;
1965            let typed: wasmtime::TypedFunc<(i32, i32), i32> = func
1966                .typed(&inst.store)
1967                .map_err(|e| format!("proxy_on_configure typed: {e}"))?;
1968            let result = typed
1969                .call(&mut inst.store, (1, plugin_config_size))
1970                .map_err(|e| format!("proxy_on_configure call: {e}"))?;
1971            if result == 0 {
1972                return Err("proxy_on_configure returned false".to_string());
1973            }
1974        }
1975
1976        Ok(inst)
1977    }
1978
1979    /// The resource limits for this plugin.
1980    pub fn limits(&self) -> &PluginLimits {
1981        &self.limits
1982    }
1983}
1984
1985/// A per-request plugin instance: a wasmtime store + instance.
1986pub struct PluginInstance {
1987    store: Store<PluginContext>,
1988    instance: wasmtime::Instance,
1989    /// Whether `proxy_on_context_create(2, 1)` has been called for the
1990    /// per-request HTTP context (SDK modules panic on a phase call for
1991    /// a context they were never told to create; fixtures without the
1992    /// export skip it).
1993    http_context_created: bool,
1994}
1995
1996/// The result of a phase callback.
1997#[derive(Clone, Debug)]
1998pub enum PhaseResult {
1999    /// Continue processing the request.
2000    Continue,
2001    /// The plugin short-circuited with a local response.
2002    LocalResponse(LocalResponse),
2003    /// The plugin trapped (out of fuel, memory error, or panic).
2004    Trap(String),
2005    /// The plugin registered one or more HTTP callouts and paused
2006    /// (DW-167). The dispatch driver performs the exchanges and calls
2007    /// [`PluginInstance::deliver_callout_response`]; the phase's
2008    /// outcome is then re-collected from the instance.
2009    Pause,
2010}
2011
2012impl PluginInstance {
2013    /// Set the current context ID (used by exports that receive a
2014    /// context_id parameter).
2015    fn set_context_id(&mut self, id: u32) {
2016        self.store.data_mut().current_context_id = id;
2017        self.store.data_mut().effective_context_id = id;
2018    }
2019
2020    /// Call `_start()` when the module exports it (SDK modules register
2021    /// their context factories there). Absent in dwara's WAT fixtures —
2022    /// a missing export is skipped, not an error. A trapping `_start`
2023    /// is PROPAGATED as the instance's error: swallowing it here would
2024    /// surface later as an unrelated panic inside
2025    /// `proxy_on_context_create` (the SDK dispatcher finds no
2026    /// registered root context), hiding the real failure.
2027    fn call_optional_start(&mut self) -> Result<(), String> {
2028        let Some(export) = self.instance.get_export(&mut self.store, "_start") else {
2029            return Ok(());
2030        };
2031        let Some(func) = export.into_func() else {
2032            return Ok(());
2033        };
2034        let Ok(typed) = func.typed::<(), ()>(&self.store) else {
2035            return Ok(());
2036        };
2037        typed
2038            .call(&mut self.store, ())
2039            .map_err(|e| format!("_start: {e}"))
2040    }
2041
2042    /// Call `proxy_on_context_create(context_id, root_context_id)` when
2043    /// the module exports it (SDK modules build their contexts there).
2044    /// Absent in dwara's WAT fixtures — a missing export is skipped.
2045    fn call_optional_context_create(&mut self, context_id: i32, root_context_id: i32) {
2046        let Some(export) = self
2047            .instance
2048            .get_export(&mut self.store, "proxy_on_context_create")
2049        else {
2050            return;
2051        };
2052        let Some(func) = export.into_func() else {
2053            return;
2054        };
2055        let Ok(typed) = func.typed::<(i32, i32), ()>(&self.store) else {
2056            return;
2057        };
2058        let _: () = typed
2059            .call(&mut self.store, (context_id, root_context_id))
2060            .unwrap_or_default();
2061    }
2062
2063    /// Ensure the per-request HTTP context exists before a phase call:
2064    /// SDK modules panic inside `proxy_on_*` for a context they were
2065    /// never told to create, so the first phase (or `on_done`) for
2066    /// context 2 first calls `proxy_on_context_create(2, 1)`. Modules
2067    /// without the export (dwara's WAT fixtures) skip it.
2068    fn ensure_http_context(&mut self) {
2069        if self.http_context_created {
2070            return;
2071        }
2072        self.http_context_created = true;
2073        self.call_optional_context_create(2, 1);
2074    }
2075
2076    /// Call `proxy_on_request_headers(context_id, num_headers, end_of_stream)`.
2077    /// Sets the request headers in the context before calling.
2078    pub fn on_request_headers(&mut self, headers: Vec<(String, String)>) -> PhaseResult {
2079        self.store.data_mut().request_headers = headers;
2080        let num_headers = self.store.data().request_headers.len() as i32;
2081        self.set_context_id(2);
2082        self.ensure_http_context();
2083        self.call_phase_export("proxy_on_request_headers", (2, num_headers, 1))
2084    }
2085
2086    /// Call `proxy_on_request_body(context_id, body_size, end_of_stream)`.
2087    /// Sets the request body in the context before calling.
2088    pub fn on_request_body(&mut self, body: Vec<u8>) -> PhaseResult {
2089        self.store.data_mut().request_body = body;
2090        let body_size = self.store.data().request_body.len() as i32;
2091        self.set_context_id(2);
2092        self.ensure_http_context();
2093        self.call_phase_export("proxy_on_request_body", (2, body_size, 1))
2094    }
2095
2096    /// Call `proxy_on_response_headers(context_id, num_headers, end_of_stream)`.
2097    /// Sets the response headers in the context before calling.
2098    pub fn on_response_headers(&mut self, headers: Vec<(String, String)>) -> PhaseResult {
2099        self.store.data_mut().response_headers = headers;
2100        let num_headers = self.store.data().response_headers.len() as i32;
2101        self.set_context_id(2);
2102        self.ensure_http_context();
2103        self.call_phase_export("proxy_on_response_headers", (2, num_headers, 1))
2104    }
2105
2106    /// Call `proxy_on_response_body(context_id, body_size, end_of_stream)`.
2107    /// Sets the response body in the context before calling.
2108    pub fn on_response_body(&mut self, body: Vec<u8>) -> PhaseResult {
2109        self.store.data_mut().response_body = body;
2110        let body_size = self.store.data().response_body.len() as i32;
2111        self.set_context_id(2);
2112        self.ensure_http_context();
2113        self.call_phase_export("proxy_on_response_body", (2, body_size, 1))
2114    }
2115
2116    /// Get the (possibly modified) request headers from the context.
2117    pub fn request_headers(&self) -> &[(String, String)] {
2118        &self.store.data().request_headers
2119    }
2120
2121    /// Get the (possibly modified) response headers from the context.
2122    pub fn response_headers(&self) -> &[(String, String)] {
2123        &self.store.data().response_headers
2124    }
2125
2126    /// Get the (possibly modified) request body from the context.
2127    pub fn request_body(&self) -> &[u8] {
2128        &self.store.data().request_body
2129    }
2130
2131    /// Get the (possibly modified) response body from the context.
2132    pub fn response_body(&self) -> &[u8] {
2133        &self.store.data().response_body
2134    }
2135
2136    /// Get the logs emitted by the plugin.
2137    pub fn logs(&self) -> &[(u32, String)] {
2138        &self.store.data().logs
2139    }
2140
2141    /// Read `len` bytes of the plugin's linear memory at `offset`
2142    /// (test support: asserting a callback's in-memory side effects).
2143    pub fn memory_bytes(&mut self, offset: usize, len: usize) -> Option<Vec<u8>> {
2144        let export = self.instance.get_export(&mut self.store, "memory")?;
2145        let memory = export.into_memory()?;
2146        memory
2147            .data(&self.store)
2148            .get(offset..offset.checked_add(len)?)
2149            .map(|s| s.to_vec())
2150    }
2151
2152    /// A snapshot of the VM-scoped shared-data map (test support: the
2153    /// cross-instance store DW-167 introduced).
2154    pub fn shared_data_snapshot(&self) -> SharedData {
2155        self.store
2156            .data()
2157            .shared_data
2158            .lock()
2159            .unwrap_or_else(std::sync::PoisonError::into_inner)
2160            .clone()
2161    }
2162
2163    /// Drain the callouts registered since the last phase/callback
2164    /// invocation (DW-167). The dispatch driver performs them in FIFO
2165    /// order (one round each).
2166    pub fn take_pending_callouts(&mut self) -> Vec<PendingCallout> {
2167        std::mem::take(&mut self.store.data_mut().pending_callouts)
2168    }
2169
2170    /// Re-enqueue callouts the dispatch driver drained but did NOT
2171    /// perform (DW-167): when the first of several queued deliveries
2172    /// re-dispatches from its callback, the un-performed remainder
2173    /// goes back AHEAD of the newly registered queue — preserving the
2174    /// global dispatch order and guaranteeing every dispatched callout
2175    /// is eventually performed (or the loop guard trips).
2176    pub fn requeue_callouts(&mut self, pending: Vec<PendingCallout>) {
2177        if pending.is_empty() {
2178            return;
2179        }
2180        let ctx = self.store.data_mut();
2181        let mut merged = pending;
2182        merged.append(&mut ctx.pending_callouts);
2183        ctx.pending_callouts = merged;
2184    }
2185
2186    /// Deliver a completed callout response and collect the plugin's
2187    /// resumed outcome (DW-167): the response's headers (`:status`
2188    /// first, hop-by-hop stripped) and body land in the context's
2189    /// callout-response stores, then the host calls
2190    /// `proxy_on_http_call_response(context_id, token, num_headers,
2191    /// body_size, num_trailers)` — the 5-parameter shape the Rust SDK
2192    /// 0.2.5 exports (its first parameter is the ABI's context slot,
2193    /// which the SDK ignores; it routes by token inside the module).
2194    /// The export returns nothing in the SDK: the plugin's decision is
2195    /// expressed through what it does inside the callback
2196    /// (`send_http_response`, header mutations, another dispatch, or
2197    /// nothing = resume Continue). A module without the export resumes
2198    /// Continue (nothing to tell).
2199    pub fn deliver_callout_response(&mut self, token: u32, resp: &CalloutResponse) -> PhaseResult {
2200        {
2201            let ctx = self.store.data_mut();
2202            ctx.callout_response_headers =
2203                std::iter::once((":status".to_string(), resp.status.to_string()))
2204                    .chain(resp.headers.iter().cloned())
2205                    .collect();
2206            ctx.callout_response_body = resp.body.clone();
2207        }
2208        let num_headers = self.store.data().callout_response_headers.len() as i32;
2209        let body_size = self.store.data().callout_response_body.len() as i32;
2210        self.set_context_id(2);
2211        self.ensure_http_context();
2212        let Some(export) = self
2213            .instance
2214            .get_export(&mut self.store, "proxy_on_http_call_response")
2215        else {
2216            return PhaseResult::Continue;
2217        };
2218        let Some(func) = export.into_func() else {
2219            return PhaseResult::Continue;
2220        };
2221        let args = (2, token as i32, num_headers, body_size, 0);
2222        // The SDK's export returns (); a hand-written fixture may
2223        // return an action — tolerate both shapes (the value is
2224        // recorded but the outcome comes from the instance state
2225        // below, matching the SDK's hostcall-driven resume model).
2226        let result: Result<u32, wasmtime::Error> =
2227            if let Ok(typed) = func.typed::<(i32, i32, i32, i32, i32), ()>(&self.store) {
2228                typed
2229                    .call(&mut self.store, args)
2230                    .map(|()| abi::ACTION_CONTINUE)
2231            } else if let Ok(typed) = func.typed::<(i32, i32, i32, i32, i32), i32>(&self.store) {
2232                typed.call(&mut self.store, args).map(|a| a as u32)
2233            } else {
2234                return PhaseResult::Trap(
2235                    "proxy_on_http_call_response has an unusable signature".to_string(),
2236                );
2237            };
2238        match result {
2239            Ok(action) => {
2240                let ctx = self.store.data_mut();
2241                ctx.action = action;
2242                if let Some(resp) = ctx.local_response.take() {
2243                    PhaseResult::LocalResponse(resp)
2244                } else if !ctx.pending_callouts.is_empty() {
2245                    // The callback dispatched another callout: pause
2246                    // again (the driver's next round picks it up).
2247                    PhaseResult::Pause
2248                } else {
2249                    PhaseResult::Continue
2250                }
2251            }
2252            Err(e) => {
2253                let fuel_left = self.store.get_fuel().unwrap_or(0);
2254                if fuel_left == 0 {
2255                    PhaseResult::Trap(format!("proxy_on_http_call_response: fuel exhausted: {e}"))
2256                } else {
2257                    PhaseResult::Trap(format!("proxy_on_http_call_response: {e}"))
2258                }
2259            }
2260        }
2261    }
2262
2263    /// Call `proxy_on_done` and `proxy_on_log` (the cleanup path).
2264    /// The SDK's `proxy_on_done` returns a bool while the WAT fixtures
2265    /// return nothing — the result is ignored either way, so both
2266    /// shapes are accepted.
2267    pub fn on_done(&mut self) {
2268        self.set_context_id(2);
2269        self.ensure_http_context();
2270        let _ = self.call_optional_i32_export("proxy_on_done", 2);
2271        let _ = self.call_optional_i32_export("proxy_on_log", 2);
2272        let _ = self.call_optional_i32_export("proxy_on_delete", 2);
2273    }
2274
2275    /// Call an optional `(i32) -> ()`-or-`(i32) -> i32` export with
2276    /// `arg`, tolerating either return shape (fixtures return nothing;
2277    /// the SDK returns a bool). Missing exports are skipped.
2278    fn call_optional_i32_export(&mut self, name: &str, arg: i32) -> Result<(), String> {
2279        let Some(export) = self.instance.get_export(&mut self.store, name) else {
2280            return Ok(());
2281        };
2282        let Some(func) = export.into_func() else {
2283            return Ok(());
2284        };
2285        if let Ok(typed) = func.typed::<(i32,), ()>(&self.store) {
2286            return typed
2287                .call(&mut self.store, (arg,))
2288                .map_err(|e| format!("{name}: {e}"));
2289        }
2290        if let Ok(typed) = func.typed::<(i32,), i32>(&self.store) {
2291            return typed
2292                .call(&mut self.store, (arg,))
2293                .map(|_| ())
2294                .map_err(|e| format!("{name}: {e}"));
2295        }
2296        Ok(())
2297    }
2298
2299    /// Call a phase export that takes (context_id, a, b) and returns an action.
2300    fn call_phase_export(&mut self, name: &str, args: (i32, i32, i32)) -> PhaseResult {
2301        let export = match self.instance.get_export(&mut self.store, name) {
2302            Some(e) => e,
2303            None => return PhaseResult::Continue,
2304        };
2305        let func = match export.into_func() {
2306            Some(f) => f,
2307            None => return PhaseResult::Continue,
2308        };
2309        let typed: wasmtime::TypedFunc<(i32, i32, i32), i32> = match func.typed(&self.store) {
2310            Ok(t) => t,
2311            Err(e) => return PhaseResult::Trap(format!("{name} typed: {e}")),
2312        };
2313        match typed.call(&mut self.store, args) {
2314            Ok(action) => {
2315                let ctx = self.store.data_mut();
2316                ctx.action = action as u32;
2317                if let Some(resp) = ctx.local_response.take() {
2318                    PhaseResult::LocalResponse(resp)
2319                } else if action == abi::ACTION_END_STREAM as i32 {
2320                    PhaseResult::LocalResponse(LocalResponse {
2321                        status: 200,
2322                        headers: Vec::new(),
2323                        body: Vec::new(),
2324                    })
2325                } else if !ctx.pending_callouts.is_empty() {
2326                    // The phase registered callouts (proxy_http_call)
2327                    // and did not short-circuit: pause for the host to
2328                    // perform them (DW-167). The returned action value
2329                    // is advisory — a plugin that dispatches a callout
2330                    // and returns Continue still pauses, because the
2331                    // phase's outcome cannot be known until the
2332                    // callout's callback ran.
2333                    PhaseResult::Pause
2334                } else {
2335                    PhaseResult::Continue
2336                }
2337            }
2338            Err(e) => {
2339                // Detect fuel exhaustion: wasmtime's trap Display does
2340                // not include the word "fuel", so check the remaining
2341                // fuel budget and annotate the message when it is zero.
2342                let fuel_left = self.store.get_fuel().unwrap_or(0);
2343                if fuel_left == 0 {
2344                    PhaseResult::Trap(format!("{name}: fuel exhausted: {e}"))
2345                } else {
2346                    PhaseResult::Trap(format!("{name}: {e}"))
2347                }
2348            }
2349        }
2350    }
2351}
2352
2353// --- Helper functions ----------------------------------------------------
2354
2355/// Validate and register one `proxy_http_call` dispatch (DW-167) into
2356/// the instance's pending list, handing out the next token. `None`
2357/// maps to `Status::BadArgument` at the import (the Rust SDK surfaces
2358/// that as `Err(Status::BadArgument)` from `dispatch_http_call`).
2359///
2360/// Validation: the URI must parse with an `http://`/`https://` scheme
2361/// and a host — the URI is the whole callout target (scheme,
2362/// authority, and optionally its own path+query), and the callout's
2363/// `Host` header derives from it. The dispatch header map's
2364/// pseudo-headers steer what they can: `:method` (default `GET`) and
2365/// `:path` (applied only when the URI carries no path of its own —
2366/// the URI-embedded target wins, and must be origin-form). `:authority`
2367/// and unknown pseudo-headers are advisory here and dropped (the
2368/// plugin shapes neither the dial target nor the Host; the same
2369/// authority-shapes-Host-only posture the request-path rewrite
2370/// enforces would need a cluster abstraction dwara does not have).
2371///
2372/// Request-head integrity (fail-closed BEFORE any network work): the
2373/// callout request head interpolates the method, the header names and
2374/// values, and the map `:path` verbatim, so each must be
2375/// splitting-safe — the method and every header name an RFC 7230
2376/// token, and no CR, LF, or NUL anywhere in a name, value, or the map
2377/// `:path`. A plugin reflecting client data into a callout cannot
2378/// smuggle a second request into the gateway-initiated connection;
2379/// violations register nothing and answer BadArgument.
2380fn register_callout(
2381    ctx: &mut PluginContext,
2382    uri: &str,
2383    headers_bytes: &[u8],
2384    body: Vec<u8>,
2385    timeout_ms: i32,
2386) -> Option<u32> {
2387    let parsed: http::Uri = uri.parse().ok()?;
2388    let scheme = match parsed.scheme_str() {
2389        Some("http") | Some("https") => parsed.scheme_str().unwrap(),
2390        _ => return None,
2391    };
2392    let host = parsed.host()?;
2393    let map = abi::deserialize_header_map_spec(headers_bytes)?;
2394    let mut method = "GET".to_string();
2395    let mut map_path: Option<String> = None;
2396    let mut headers = Vec::with_capacity(map.len());
2397    for (name, value) in map {
2398        match name.as_str() {
2399            ":method" if !value.is_empty() => method = value,
2400            ":path" if !value.is_empty() => map_path = Some(value),
2401            n if n.starts_with(':') => {} // :authority and unknown pseudo-headers dropped
2402            _ => headers.push((name, value)),
2403        }
2404    }
2405    if !super::callout::is_rfc7230_token(&method) {
2406        return None;
2407    }
2408    if let Some(p) = &map_path {
2409        if !super::callout::is_head_safe(p) {
2410            return None;
2411        }
2412    }
2413    for (name, value) in &headers {
2414        if !super::callout::is_rfc7230_token(name) || !super::callout::is_head_safe(name) {
2415            return None;
2416        }
2417        if !super::callout::is_head_safe(value) {
2418            return None;
2419        }
2420    }
2421    let mut effective = format!("{scheme}://{host}");
2422    if let Some(port) = parsed.port_u16() {
2423        effective.push_str(&format!(":{port}"));
2424    }
2425    let target = match parsed.path_and_query() {
2426        Some(pq) if pq.as_str() != "/" => pq.as_str().to_string(),
2427        _ => match map_path {
2428            Some(p) if p.starts_with('/') => p,
2429            _ => "/".to_string(),
2430        },
2431    };
2432    effective.push_str(&target);
2433    ctx.callout_token_counter = ctx.callout_token_counter.saturating_add(1);
2434    let token = ctx.callout_token_counter;
2435    ctx.pending_callouts.push(PendingCallout {
2436        token,
2437        uri: effective,
2438        method,
2439        headers,
2440        body,
2441        timeout_ms: timeout_ms.max(0) as u32,
2442    });
2443    Some(token)
2444}
2445
2446// Unsupported-hostcall stubs, one per proxy-wasm spec ARITY (wasmtime's
2447// typed linking requires the import signature to match exactly; the
2448// Rust SDK declares every import with its spec signature even when the
2449// plugin never calls it). All return Status::InternalFailure (10).
2450// SDK 0.2.5 mapping of that status, exactly (hostcalls.rs):
2451//   - clean Err(Status::InternalFailure): the two remaining callout
2452//     dispatchers — dispatch_grpc_call and open_grpc_stream
2453//     (dispatch_http_call is implemented, DW-167).
2454//   - panic! on any non-Ok status: every other wrapper, including
2455//     send_grpc_stream_message and cancel_grpc_call/cancel_grpc_stream
2456//     (they map only BadArgument/NotFound to Err), set_tick_period,
2457//     the shared-queue calls, and call_foreign_function.
2458// A panicking wrapper traps the module, which the host reports as a
2459// plugin failure (trap -> 500 on the referencing route) — host-safe
2460// either way; the point of returning InternalFailure rather than some
2461// other status is that it is the one status the largest set of SDK
2462// wrappers handles without trapping. It is NOT a general "clean Err"
2463// for all callers.
2464fn stub_unsupported_1(_: wasmtime::Caller<PluginContext>, _a: i32) -> i32 {
2465    10 // Status::InternalFailure
2466}
2467
2468fn stub_unsupported_3(_: wasmtime::Caller<PluginContext>, _a: i32, _b: i32, _c: i32) -> i32 {
2469    10 // Status::InternalFailure
2470}
2471
2472fn stub_unsupported_4(
2473    _: wasmtime::Caller<PluginContext>,
2474    _a: i32,
2475    _b: i32,
2476    _c: i32,
2477    _d: i32,
2478) -> i32 {
2479    10 // Status::InternalFailure
2480}
2481
2482fn stub_unsupported_5(
2483    _: wasmtime::Caller<PluginContext>,
2484    _a: i32,
2485    _b: i32,
2486    _c: i32,
2487    _d: i32,
2488    _e: i32,
2489) -> i32 {
2490    10 // Status::InternalFailure
2491}
2492
2493fn stub_unsupported_6(
2494    _: wasmtime::Caller<PluginContext>,
2495    _a: i32,
2496    _b: i32,
2497    _c: i32,
2498    _d: i32,
2499    _e: i32,
2500    _f: i32,
2501) -> i32 {
2502    10 // Status::InternalFailure
2503}
2504
2505#[allow(clippy::too_many_arguments)]
2506fn stub_unsupported_9(
2507    _: wasmtime::Caller<PluginContext>,
2508    _a: i32,
2509    _b: i32,
2510    _c: i32,
2511    _d: i32,
2512    _e: i32,
2513    _f: i32,
2514    _g: i32,
2515    _h: i32,
2516    _i: i32,
2517) -> i32 {
2518    10 // Status::InternalFailure
2519}
2520
2521#[allow(clippy::too_many_arguments)]
2522fn stub_unsupported_12(
2523    _: wasmtime::Caller<PluginContext>,
2524    _a: i32,
2525    _b: i32,
2526    _c: i32,
2527    _d: i32,
2528    _e: i32,
2529    _f: i32,
2530    _g: i32,
2531    _h: i32,
2532    _i: i32,
2533    _j: i32,
2534    _k: i32,
2535    _l: i32,
2536) -> i32 {
2537    10 // Status::InternalFailure
2538}
2539
2540/// The body of dwara's original `proxy_send_http_response` hostcall
2541/// (7 params; the WAT fixtures and host tests use this spelling). The
2542/// spec name `proxy_send_local_response` has a different signature and
2543/// is registered separately in [`WasmEngine::new`]. The header
2544/// argument here deliberately keeps dwara's LEGACY big-endian map
2545/// layout (abi::deserialize_header_map): modules written against this
2546/// spelling (dwara's example plugins) serialize that way, while the
2547/// spec-named hostcalls speak the SDK layout. The additive rule: the
2548/// legacy spelling keeps the legacy wire format.
2549#[allow(clippy::too_many_arguments)]
2550fn send_http_response_impl(
2551    mut caller: wasmtime::Caller<PluginContext>,
2552    status: i32,
2553    headers_ptr: i32,
2554    headers_size: i32,
2555    body_ptr: i32,
2556    body_size: i32,
2557    _trailers_ptr: i32,
2558    _trailers_size: i32,
2559) -> i32 {
2560    let memory = match caller.get_export("memory") {
2561        Some(wasmtime::Extern::Memory(m)) => m,
2562        _ => return 1,
2563    };
2564    let headers = if headers_ptr > 0 && headers_size > 0 {
2565        let data = match memory
2566            .data(&caller)
2567            .get(headers_ptr as usize..(headers_ptr as usize + headers_size as usize))
2568        {
2569            Some(slice) => slice.to_vec(),
2570            None => return 1,
2571        };
2572        match abi::deserialize_header_map(&data) {
2573            Some(h) => h,
2574            None => return 1,
2575        }
2576    } else {
2577        Vec::new()
2578    };
2579    let body = if body_ptr > 0 && body_size > 0 {
2580        match memory
2581            .data(&caller)
2582            .get(body_ptr as usize..(body_ptr as usize + body_size as usize))
2583        {
2584            Some(slice) => slice.to_vec(),
2585            None => return 1,
2586        }
2587    } else {
2588        Vec::new()
2589    };
2590    let ctx = caller.data_mut();
2591    ctx.local_response = Some(LocalResponse {
2592        status: status as u16,
2593        headers,
2594        body,
2595    });
2596    ctx.action = abi::ACTION_END_STREAM;
2597    0
2598}
2599
2600/// The shared body of `proxy_get_current_time` /
2601/// `proxy_get_current_time_nanoseconds` (dwara's spelling and the
2602/// proxy-wasm spec spelling): nanoseconds since the Unix epoch.
2603fn get_current_time_impl(
2604    mut caller: wasmtime::Caller<PluginContext>,
2605    return_value_ptr: i32,
2606) -> i32 {
2607    let memory = match caller.get_export("memory") {
2608        Some(wasmtime::Extern::Memory(m)) => m,
2609        _ => return 1,
2610    };
2611    let now = std::time::SystemTime::now()
2612        .duration_since(std::time::UNIX_EPOCH)
2613        .map(|d| d.as_nanos() as u64)
2614        .unwrap_or(0);
2615    let bytes = now.to_le_bytes();
2616    if memory
2617        .data_mut(&mut caller)
2618        .get_mut(return_value_ptr as usize..(return_value_ptr as usize + 8))
2619        .map(|dst| dst.copy_from_slice(&bytes))
2620        .is_none()
2621    {
2622        return 1;
2623    }
2624    0
2625}
2626
2627/// Seed for the WASI `random_get` stub: wall-clock nanoseconds mixed
2628/// with a process-global counter (two plugins seeded in the same
2629/// nanosecond still diverge). Not cryptographic.
2630fn wasi_random_seed() -> u64 {
2631    use std::sync::atomic::{AtomicU64, Ordering};
2632
2633    static COUNTER: AtomicU64 = AtomicU64::new(0);
2634    let now = std::time::SystemTime::now()
2635        .duration_since(std::time::UNIX_EPOCH)
2636        .map(|d| d.as_nanos() as u64)
2637        .unwrap_or(0);
2638    now ^ COUNTER
2639        .fetch_add(1, Ordering::Relaxed)
2640        .wrapping_mul(0x9E3779B97F4A7C15)
2641}
2642
2643/// Write an i32 value to a pointer in plugin memory.
2644fn write_i32_to_memory(
2645    memory: &wasmtime::Memory,
2646    mut store: impl wasmtime::AsContextMut,
2647    ptr: i32,
2648    value: i32,
2649) -> Result<(), String> {
2650    if ptr < 0 {
2651        return Err("negative pointer".to_string());
2652    }
2653    let bytes = value.to_le_bytes();
2654    let mut ctx = store.as_context_mut();
2655    memory
2656        .data_mut(&mut ctx)
2657        .get_mut(ptr as usize..(ptr as usize + 4))
2658        .ok_or("pointer out of bounds")?
2659        .copy_from_slice(&bytes);
2660    Ok(())
2661}
2662
2663/// Allocate `size` bytes in the plugin's linear memory by calling the
2664/// plugin's `proxy_on_memory_allocate` export (the standard proxy-wasm
2665/// allocation pattern). Falls back to `memory.grow` if the export is
2666/// not present.
2667fn allocate_in_plugin(
2668    caller: &mut wasmtime::Caller<PluginContext>,
2669    memory: &wasmtime::Memory,
2670    size: usize,
2671) -> Result<usize, String> {
2672    // Try the standard proxy_on_memory_allocate export first.
2673    if let Some(wasmtime::Extern::Func(alloc_func)) = caller.get_export("proxy_on_memory_allocate")
2674    {
2675        if let Ok(typed) = alloc_func.typed::<(i32,), i32>(&mut *caller) {
2676            let ptr = typed
2677                .call(&mut *caller, (size as i32,))
2678                .map_err(|e| format!("proxy_on_memory_allocate: {e}"))?;
2679            if ptr > 0 {
2680                return Ok(ptr as usize);
2681            }
2682        }
2683    }
2684    // Fallback: grow the memory by one page and use the new space.
2685    let current = memory.data_size(&mut *caller);
2686    let needed = current + size;
2687    let pages_needed = (needed - current).div_ceil(65536);
2688    if pages_needed > 0 {
2689        memory
2690            .grow(&mut *caller, pages_needed as u64)
2691            .map_err(|e| format!("memory.grow: {e}"))?;
2692    }
2693    Ok(current)
2694}